Direct answer
Exceptions make error handling implicit and easy to accidentally skip, but keep the success-path code uncluttered; explicit error codes or return values force every caller to consciously handle the failure case, at the cost of more verbose code at every call site. Which is idiomatic depends heavily on the language: Go and Rust favor explicit returns, Java and Python favor exceptions, and Node.js uses a mix depending on whether the code is synchronous or promise-based.
Structured elaboration
Exceptions (Java, Python). An exception unwinds the call stack automatically to the nearest handler, which keeps the success path readable (no error-checking boilerplate after every call) but makes it easy for a caller to simply forget to catch something, especially in a language like Java where checked exceptions are used inconsistently, or Python where nothing in a function's signature indicates what it might raise at all. The propagation is implicit: an uncaught exception silently travels up through every intermediate frame until something catches it or the program crashes, which can hide exactly where a failure needs handling until it's discovered in production.
Explicit error codes and return values (Go, Rust). Go's (result, error) return convention and Rust's Result<T, E> type both force the caller to explicitly acknowledge the possibility of failure at every single call site, since the value cannot be used without either handling or explicitly ignoring the error component. This makes control flow highly visible (every fallible call is visually marked in the code) at the cost of real verbosity: a chain of five fallible operations in Go often means five if err != nil { return err } blocks in a row.
Node.js: a genuine mix. Synchronous code commonly throws; asynchronous code built on Promises resolves or rejects, and an unhandled promise rejection is its own distinct failure mode from an uncaught synchronous exception, one that historically could fail silently unless the runtime was configured to treat it as fatal. A codebase mixing callback-style, Promise-style, and async/await code can end up with three different error-propagation conventions active at once, which is itself a common source of bugs.
Designing an API to make error cases explicit and testable. Regardless of the language's own convention, a well-designed API surface names its possible failure modes in its own contract, not just in its implementation: a function's documentation (or its type signature, for languages that support a Result-like return type even where the language itself favors exceptions) should say what can go wrong, so a caller does not have to read the implementation to find out. This can be tested directly: for each documented failure mode, a unit test should assert the specific error type or code produced, not just that "an error occurs".
Worked example
The same operation, fetch-a-user-then-update-their-balance, in Go versus Python: in Go, user, err := getUser(id); if err != nil { return err }; err = updateBalance(user, amount); if err != nil { return err } makes both fallible steps visually explicit, and it is a compile error to use user before checking err. In Python, user = get_user(id); update_balance(user, amount) reads more cleanly, but nothing in the code signals that either call can raise, and a caller three call-frames up who forgot to wrap this in a try/except will simply see an unhandled UserNotFoundError propagate as an unhandled exception, potentially crashing an entire request handler for a case that a Go-equivalent caller would have been forced to at least acknowledge.
Trade-offs and pitfalls
The most common mistake when working against the grain of a language's own idiom is inconsistency: mixing exceptions and error-code returns within the same Python or Java codebase (some functions raise, some return None or a special error object on failure) forces every caller to remember, function by function, which convention applies, which is worse than either convention applied consistently. A second common mistake specific to Go is checking err != nil and then continuing to use a possibly-invalid result anyway due to a copy-paste error, since Go's convention does not prevent that the way a language with a true sum type like Rust's Result does.