I love how [[Go]] started off with *exceptions bad* and slowly re-implemented exceptions. > [!quote] [[2025-06-03]] - https://go.dev/blog/error-syntax > For the foreseeable future, the Go team will stop pursuing syntactic language changes for error handling. We will also close all open and incoming proposals that concern themselves primarily with the syntax of error handling, without further investigation. # Background on Exceptions > [!note] see also [[On Exception]] Why are exceptions bad anyway? * **Exceptions are bad in C++ in many ways** - The worst thing is that if an exception is raised in a constructor, then there's a memory leak. Scott Meyers' *Effective C++* has many gotchas around exceptions. * C++ exceptions doesn't have much metadata (C++ and runtime metadata is an oxymoron). In particular, they don't contain stack traces (Technically this applies to [[JavaScript]] also, as you can throw any object as an exception, though one seldomly does so). * Talking about JavaScript, exceptions were discouraged in JS also. Due to JS's callback-style programming, it's very easy to have uncaught exceptions. Worse; uncaught exceptions break the callback chain. * **Checked Exception** have their own complexities; using [[Java]] as an example, checked exceptions are regularly wrapped into an unchecked `RuntimeException` purely for convenience. * Exceptions allow for sloppy code. For example, one can write: ```java try { a() b() c() } catch (Exception e) { ... } ``` For this code, exception handler doesn't know much. Which operation succeeded, failed, or not attempted. However, in many runtime code, this "sloppiness" may be a feature not a bug. # The Good Parts ## `defer` statement As documented on [[On Exception]], `try-finally` is the most useful form of exception handling. Go has a native support for it via `defer`. In most cases, `defer` is a perfect solution for resource cleanup, with few caveats. - `defer` unfortunately has the function scope. Thus, it cannot be used inside a loop (it is almost always a bug) or a long code block (resource is held for longer than needed; this may be a serious bug for mutexes). - Overhead associated with `defer` that is now resolved. # Errors are value - fallacy This is an example of [[Decision Problem]] fallacy. Modern exceptions are quite idiomatic. In particular they contain **stack traces** which points to the location of the exception. Stack trace (and the type of the exception alone) can be sufficient for a successful debugging. That's why a service like [[Sentry]] is so popular. comparing errors via equality only works for the simplest cases. It really needs to have C errno semantics. If a struct is returned, then you want to perform some version of pattern matching (which is what `errors.As` is doing). # Go Error Evolution [errors package](https://pkg.go.dev/errors) * Started as "pure value", where one can use the equals operator to compare. * Even from the beginning, certain errors such as [SyscallError](https://pkg.go.dev/os#SyscallError) needed to return a richer error than just a value. To implement "try-catch", one needs to do the following: ```go res, err := operation() if err != nil { if casted, ok := err.(Errortype); ok { // handler here. } } ``` * No Stack Trace = impossible to debug errors easily. * No Parent Error = individual libraries had to implement their own version of this. * Errors are not typesafe as they're always defined as type `error`. Imagine if every function in java can only return `RuntimeException`. * This, along with `interface{}`, was the biggest untyped soup of early Go. * People started to return wrapped errors - but this breaks all the casting-based error handling. * `errors.As` and `errors.Is` are added to support un-wrapping. * I don't remember exception wrapping being a big problem in Java. * In Java, as `catch` statement is semantically powerful; as one doesn't want to break this semantic, one would re-throw the same exception (in fact I would argue that this is one of the weak points of weakly typed languages. typed catch are powerful. Even between js (no typed catch) and python (typed catch due to its [[OOP#Classical OOP]]) this is a big difference). * Re-throwing preserves the stack trace too; which is one more reason why rethrowing is desirable. * However, as go didn't have stack traces in error objects, engineers were aggressively wrapping errors to gain as much metadata as possible. # Errors.As `errors.As` is very un-ergonomic in go; it requires you to take a pointer of an interface, a pattern seldom encountered by a developer. There's also the question around "should I use the pointer or non-pointer version of error". If Type implements `Type.Error()` and `*Type.Error()`, then both `Type` and `*Type` satisfies the `error` interface. If a library is haphazard at returning either version, then `As` becomes difficult to use. # Per-release evolution > [!note] [[Go - Versions]] https://pkg.go.dev/errors, introduced the concept of `Unwrap()`. ## 1.13 - `errors` package is introduced, with `Unwrap`, `Is`, and `As`. ## 1.20 * [Support for wrapping multiple errors](https://tip.golang.org/doc/go1.20#errors) * [context.With...Cause](https://tip.golang.org/doc/go1.20#minor_library_changes) * context cancellation now has causes. * THIS IS JUST LIKE A CHAINED EXCEPTION. # Third-party packages Go standard library still didn't endorsed errors with stack traces. Post Go 1.13, these packages are mostly irrelevant but the stack trace functionality is still irreplaceable. https://github.com/go-errors/errors - "errors with stack traces" https://github.com/pkg/errors - archived. provided errors with `Unwrap()` and stack traces. https://github.com/cockroachdb/errors, specifically the [RFC](https://github.com/cockroachdb/cockroach/blob/master/docs/RFCS/20190318_error_handling.md) provides a good motivation around errors. Most companies maintain their own; Both Plaid and Chronosphere had its own libraries (inheriting from `m3db`). Chronosphere forced `errors.Propagate` everywhere via linters. https://olivernguyen.io/w/namespace.error/ - *How We Centralized and Structured Error Handling*