Earlier quoted context omitted.
Go's preference for 3 over 4 is a big chunk of the reason I've never used it in anger. Rust's situation is slightly different; code typically uses a monad (Result ) for error propagation, which is mostly isomorphic to exception throwing, but more verbose. And Rust has macros to cope with some of the verbosity.
It may be isomorphic to exception handling, but it promotes a meaningfully-different human style. The exception-based isomorphism to the usual returned-error handling style would look something like: try: a_single_statement_here() except: if exception.type == "A": #yada yada elsif exception.type == "B": #yada yada else: raise Exceptions by contrast encourage putting a lot of statements together, and much less frequen…
A server connected to a network socket is very close to the wire protocol, and the potential error states are fairly well known and documented. Head up the abstraction stack deep into business logic and add a dose of third-party libraries and some high-level application structure (i.e. indirection and abstraction boundaries) and the world gets a lot more fuzzy.
I concur re GUI; I think this comes from the complexity of the GUI framework - "don't call us, we'll call you" - GUI code is typically a thin seam between two sets of framework call stacks.
I wouldn't advocate exceptions for a kernel, or a database server. The more code between you and the hardware, and the more code that you use that you don't control, the more I feel the need for exceptions to communicate error information. The higher up the stack you go, it's more likely that human intervention will be required to solve the problem - in a server situation, log the error and tell the user that support has been informed about the problem. Etc.