Why Go's Error Handling Is Awesome
rauljordan.com
rauljordan.com
int foo(int * return_value){ …; return 0 }
That's what Go does. There is no "error handling" baked into the language, aside from panics, it's a convention. It's not "awesome", it's primitive. You can do that in any language, and you don't when you have access to more evolved constructs.Yes it is exactly what Go does. Look, I'm "returning" an "out" parameter from my C function here. I can also return an "out" error in form of a "struct * error_return" if I wanted to, with a char * member or whatever.
This is not error handling baked in C either, just like Go. There is nothing "awesome" with doing that. I could do that in any language and nobody would call that "awesome".
Golang's errorhandling, I wouldn't say it's great, but it let's you be explicit about the errorhandling itself, which may lead to performant quality code. I believe it follows C errorhandling by design, but answers the age old "wish I could return an error in addition to return value here". Golang grants that wish, and even provides some compile errors if error value is not assigned to. Handling the error is left as an exercise though, so code shouldn't be too complex.
Index() returning -1 is natural for typical usage, when following conventions such as used in C-type languages. It seems to be a feature, appreciated by many, though with costs and benefits attached.
See my comment here:
https://news.ycombinator.com/item?id=23759196
> You can attach additional data to an error, and in recent versions of go you can wrap errors in other errors.
Yes, with a function added to the std lib after people used a 3rd party lib for years to do the same thing. Nothing to do with a change of language structure or features.
case some_operation() do
{:ok, result} -> # do something with result
{:error, error} -> # handle the error
endIt's extremely verbose and relies on functions returning a value OR an error. If I have an error in a function that returns a user or error, I still have to do "return &User{}, err". Or if I have no error, then it's "return user, nil". Ugly and verbose.
I really don't want to sound like a rust snob, but when it comes to errors and error handling, I only really want to use rust because it has Result<T, E> and Option<T> generic types. I don't have to do a million "if err ...", I can simply use ? inside a function that returns a Result<T, E> and kick it down the chain until I want to handle it. Then I can do a "match" statement on a Result to unwrap it and handle all cases.
To give an example, look at how succinct this is. [0] In that function I make 3 calls that could give an error and just put a ? on two of them, and I handle the last call manually with a match statement. Now compare that to this function in golang [1], where I have to make several calls that could error. (Admittedly this golang code could be better, but...)
Rust's system is not only more convenient but it forces me to handle error cases. That strictness is very annoying when I just want code to compile, but I'm sure it has saved me from plenty of bugs. With i.e. Ruby I ran into bugs at runtime quite frequently because I didn't realize I wasn't handling X or Y case. Golang at least has static types but it's still quite easy to forget to handle certain cases.
I say this having written a fair amount of golang code and a lot of rust code. Golang is not a bad language by any measure; I find myself reaching for it a lot.
[0]: https://github.com/azah/hangeul-rs/blob/master/src/lib.rs#L3...
[1]: https://github.com/azah/caddy_logingov/blob/master/handler.g...
(PS: I agree that this sort of "explicit" error handling is vastly superior to exceptions, but in my point of view, Rust and Go are pretty much identical, at least when compared to exceptions).
match maybe_error() {
Ok(t) => {}, // we succeeded
Err(e) => {}, // we did not succeed
}Result itself is an enum with Ok and Err variants. [0] Some functions use Option<T> which will either be Some(T) or None.
But you can create the "gist" of this sort of return-based error handling even in C, by returning a struct which contains both an error code and a value-payload, it's just not as convenient (because C lacks generics), or as "safe" (because one can access the value even when an error is returned), but it still would be better than "old-school" C code which splits the error-code and success-value into a simple return value and an out-pointer, and arguably also better than C++ exceptions.
Algebraic data types, or sum types, or discriminated unions.
GP also forgot to mention that Rust (among other languages with support for ADTs) force you to handle all possible values, so you simply can't forget to handle an error.
This is such an important point, and I know people may think its outrageous that this could happen, but I work with a bunch of talented programmers, and still we've had the occasional bug crop up in our Golang codebase where we have a `val, err = doSomething()` without an ensuing `if err != nil { }` check which was missed by the programmer and by the reviewer. Or even better, we've had `val, _ = doSomething()` occur, the result of debugging to get around the unused variable compilation error.
It's 2020. Just like nil-pointers, this is an issue that is for the most part _solved_ and shouldn't exist in modern languages. Our programming languages should be constructed with the fact that we're fallible humans in mind.
This is one of the reasons so many people are calling for Go to get generics.
> or as "safe" (because one can access the value even when an error is returned)
Which is one of the key things one wants from an error handling mechanism: the inability to misuse it.
Even if it gets them at some point, the language still needs support for algebraic types, which not surprisingly, the golang authors "don't see it as useful".
Go's error handling is atrocious. Not because errors are return types, or because of boilerplate for propagating them. Because they do not communicate the most important invariant: that either there is an error or there is a successful value. A tagged union is better. Exceptions are better. Even C's error code system in a twisted way can be thought of as slightly better because at least there is one dedicated spot for the error so you can shove the function call in the if statement directly.
Not to mention, as an attack against exceptions, he writes code that logs errors to the console and does nothing else, and then complains that it logs errors to the console and does nothing else, as though that's a flaw in the exception system and not in the code he just wrote. No shit, if you write Java code to dump an unreadable stack trace, an unreadable stack trace is what's getting dumped. How this eludes the author, or why he thinks it's a benefit to not have a stack trace when you want one, is beyond me.
value tuples are not error handling.
it is not sufficient to rely on an IDE or linter to catch lack of error-case handling. they are semantically identical, and in practise worse, to exceptions: at least exceptions will bubble up, and be caught.. somewhere.
and i'm not saying exceptions are the solution. they really are awful. in my opinion, 'either'/'oneof' types are the best solution here: if something returns either a failure of a success value, then you as a developer are forced to handle the failure condition:
// pseudo c#
either<error, int> result = get_string_length(str);
return reslut.match(left: _ => 0, right: x => x);
you may choose to throw an exception, you may choose to return a default. but you absolutely cannnot _not_ handle this function's failure condition (sorry, double negative..).By this definition is there a language that doesn't have "error handling?"
For cases like "user not found" and "incorrect password", ideally I'm going to have proper option types that statically check I'm handling the success/failure flows. Which "errors" go into option types and which errors go into exceptions is a design choice that's dependent on the domain.
I'm guessing people that don't like exceptions are working in domains that need lots of careful local error handling?
I'm not seeing where the problem is to be honest. Every approach can be abused somehow if a programmer wants to excessively suppress errors.
2. Compared to something like ZIO (http://zio.dev) from the Scala world, Go's error handling is amateurish. With ZIO you represent your methods as such:
def fetchFromDB: IO[DatabaseError, MyObject]
Now DatabaseError can be anything e.g. an exception, a string, an enum, a custom error class or even nothing at all if there will never be an error. Now you can chain functions together transforming errors along the way as if they were normal values. Both powerful and elegant.I still feel Java is the baseline on how to bring exceptions to the language design and I believe part of the success of Java as a stable and predictable language is because of it. I feel a lot of the times new programming languages try to avoid java exception handling system because it's too verbose or brittle just to arrive in some solution that trade out the verbose checked code but doesn't deliver any feature on the same level.
> or me this is a big problem of using Either, it treats error as one alternative of the code rather than something unusual that needs to be handled ASAP.
But that depends on the use-case. There are several 'kinds' of errors and some of them have to be treated - as you put it - "one alternative of the code". If it's a communication error, you might want to retry instead of fail immediately, for example. If you are validating user input, retrying doesn't make sense, so there you want to fail fast.
> If the compiler doesn't enforce it 99% of the cases nobody care to treat it at the right application layer which makes it very difficult to debug.
Not quite sure what you mean by that. Care to rephrase?
> in Scala there's no standard way to handle exceptions,
But of course. It's `try (f()) catch { case e: .... }`. That's one and only way to handle exceptions in Scala. After an exception has been caught, you of course can lift the errors into any data structure you want (Try, Either, IO, ZIO,...).
> But of course. It's `try (f()) catch { case e: .... }`. That's one and only way to handle exceptions in Scala. After an exception has been caught, you of course can lift the errors into any data structure you want (Try, Either, IO, ZIO,...). That may be true for Java Exceptions but not 100% true for the generic concept of errors. You have try-catch which was inherited from JVM but most of my experience working in Scala people would call it a bad pattern and prefer using Either monads. The problem then being that Either try to solve something on the user code space that is already solved on the JVM level. And in a very poor way, think how in Java you can do a try-catch with type unions on the catch clause, or how you can catch and easily transform an exception into a totally different one and declare your signature on the most restrictive exception type. And the list of awkwardness goes on, see if you would use the same strategy to handle exceptions on the following examples:
- code dealing with Java libraries
- code dealing with Futures
- code dealing with Either or other monads with left/error side
- code that have to interact with different helper libraries (like cats or shapeless)
- Either.map over a value that could produce a RuntimeException (e.g atoi)
It's not trying to revolutionise the world.
Now it does make code more verbose since 90% of the time, the answer to "where should I handle this error" is "not right here" and with go/errbacks, you have to be explicit in that case while it is the default for exceptions.
For Javascript, I recently wrote a module that allows you to jump between styles very easily: https://github.com/bessiambre/casync#readme
- 63ms (async)
- 100ms (casync)
so it's almost twice as slow as native async await in this example.
Also note that async has the benefit of language level integration and optimization. It gets to use v8 c++ functions like "EnqueueMicrotask" instead of process.nextTick. If casync had access to all this, it would likely run even faster.
I code in Go about 50% of my time, while the other 50% is Python. There, the code looks more elegant at the first sight, and yes, is not so-called "polluted" with error handling, but it takes more time to understand what it precisely does in all possible cases. Overall, I really started to prefer the Go, more explicit, way.
It's made even more frustrating in that it's very difficult to convey the pragmatic differences between those two approaches to someone who hasn't had the chance to use both. Both seem to be a way of capturing errors in the type system, but the latter really is just dramatically more ergonomic.
So people talk past each other and around and around we go.
The Rust and Go error handling approach is only superficially different. Both:
- are statically checked
- stick out to the caller
- are just values (instead of idiosyncratic control mechanisms)
- allow the caller to handle them at their discretion
The big difference on how you interact with errors are orthogonal to that. In Rust you get to use pattern matching and a whole bunch of useful traits and sugar, both for propagating them and for panicking. In Go you write these if-blocks. So in Rust you end up with something more involved/complex that conveys its semantics clearer. Go is much more simple and accessible but also less expressive. (Another good example of this is the whole story about iterators in Rust vs. just for-loops in Go.)
From my perspective the difference in error handling is more about the languages in general, because both just give you error-values. From a general perspective. There certainly are subtleties, which I'm ignoring here.
* Go returns a product type, that is, you get a value AND an error
* Rust returns a sum type, that is, you get a value OR an error
This doesn't mean one or the other is inherently better, though I do admit a strong personal bias for the Rust way. The tradeoff is basically that sum types really need generics to work well, IMHO. This means that Go can drop a lot of the other machinery Rust has that makes its system work. That's the core tradeoff.
This matters because of, for example, your first bullet point. Yes, both languages statically check their errors, but the ways in which they do so and the properties you get from said static checking are significantly (in my mind) different.
In Go you get a flat recipe of instructions. You find an error then exit the function somehow, usually via returning. In the subsequent blocks the soundness is sometimes only implied AKA you 'know' you handled that error before and now you can assume that the other part of your 'product type' has a certain shape but they are inherently separated types.
In Rust you get matching and function expressions (often from traits) flowing through a tree. There is a type soundness to this: Each branch in isolation is a statically checked part of the whole tree.
(I have personally never used an advanced IDE for Go, but I can at least imagine that a sufficiently powerful one can catch quite a few of these cases as well.)
Hold on, in the Rust world it's understood that the Rust and checked exception error handling approaches are only superficially different (hence recurring proposals to add exception-like syntactic sugar [0]). Does that mean Go's error handling approach is only superficially different to checked exceptions?
[0] https://github.com/rust-lang/rfcs/blob/master/text/0243-trai...
Is that really so? I read this differently. The RFC you linked goes in-depth about how they are different from both normal Rust error handling and typical exception handling.
Mostly, though, it was the debugging ecosystem that was the bigger issue with error handling. With the version I'm writing in PHP/Laravel, stack traces are very clear about where they happen and involving what components. When I plug in Sentry, this makes debugging production issues much easier.
Go's code is more elegant, more performant, and is fun to write, but maintaining PHP in production is much nicer and less time-consuming.
For a project like this, where it's just something I do in my spare time for fun, not having to spend a lot of time managing production is important.
† https://www.techempower.com/benchmarks/#section=data-r19&hw=...
if err := foo(x); err != nil {
return fmt.Errorf("foo: bad argument %s", x)
} return fmt.Errorf("ListThings failed: %w", err)
which is a different error type, but the caller can downcast it into the original filesystem error type (even across multiple layers of wrapping) if they're interested in specifically these types of errors.It's funny you use PHP here, as PHP's errors can be problematic in various cases cases on account of making it impossible to get some information from them (like why an fopen() call failed, for example). Never mind the whole "errors" vs. "exceptions" schism (which was improved somewhat with PHP 7, but still has various cases where it's less-than-elegant).
This is de-facto moving language specification/design to an outside project... that linter should be built into the compiler.
(I don't disagree with the idea, but I've not done much code in Go (less than Rust, Python, C++, c# and Java -- probably in that order)
Where Go's error handling falls down is that it still doesn't force you to handle the errors. Result sum types like Rust and Swift so much better than anything else that I've used that I actually think that other languages (including Go, but also Java/C#/JavaScript/etc) ought to consider adding the feature and making it idiomatic.
Go's if (err != nil) { return err; } and Rust's ? both do that.
You could imagine a variant of Java where you had to write this:
Byte readOptionalByte(InputStream in) throws IOException {
int flag = try(in.read());
if (flag == 0) return null;
else return (byte) try(in.read());
}
Which might be better.Unchecked exception based error handling, of course, is irretrievably broken.
The mental model I have with Java unchecked exceptions it that all methods, by default, have "throws UncheckedException" at the end of it. And since the calling line of code is also a method, it doesn't need to explicitly handle it.
What would be nice is a means to mark a method as "Cannot throw exceptions of any kind", removing the unspoken "throws UncheckedException" from the signature and forcing that method to internally handle all unchecked exceptions- enforced by the compiler.
Perhaps with the coming pattern matching to Java, we'll be able to resolve this with Return value wrapping types that are One-Of (return value type, Exception type 1, Exception type 2, etc...). Then you could call the method, and pattern match the result by type.
err := foo()
if err != nil {...}
err = bar() // accidentally ignored
err = baz()
if err != nil {...}For errors, you should do error handling, which means you want the compiler to make sure you don't miss them.
For bugs, you want to let the program bomb out, either aborting entirely or stopping at some high-level boundary from which it's possible to sanely continue. In Java, unchecked exceptions are a mostly adequate mechanism for that. Rust's catch_unwind is better, because it makes stronger guarantees [1]. Erlang's approach of terminating the thread and throwing away its heap is also very good, if you can apply that.
[0] http://joeduffyblog.com/2016/02/07/the-error-model/#bugs-are...
[1] https://doc.rust-lang.org/std/panic/fn.catch_unwind.html
try:
x = json['foo']['bar'][i]
catch OOB:
handle error
than to if not (json is a dict and
'foo' in json and
json['foo'] is a dict and
...):
handle error
x = json['foo']['bar'][i]
even though in the former, unchecked exceptions are used for non-bugs.The fact is, there are different classes of errors that need to be handled differently. Even the standard go runtime library operates on this principle. Some errors are returned to the user as a return value, while others trigger exceptions (called "panics" in go).
Trying to shoehorn all errors into a single mechanism is a fool's errand.
A maybe type would have been a much better solution. I wish people would just accept that go's error handling is a stopgap solution instead of pretending it's a good (or the best) solution.
I'd bet a substantial amount that go in 10 years ends up with pattern matching and sum types.
I am not familiar with Rust, but looking around the docs on Read [https://doc.rust-lang.org/std/io/trait.Read.html#errors] I see:
> If an error is returned then it must be guaranteed that no bytes were read.
Could someone elaborate how a system can satisfy this guarantee, seems impossible...?
NormalRead | ShortRead | Error
This case is also an example of why go’s idiom of using error value or return value doesn’t always work.
If you had a maybe type and a general sum type it would be clear when a function was going to return either or both.
Value Error ValueAndError
func foo()(value,err) can return any combination of valid or invalid value and error, so (nil, nil) would be possible, or ("valid value", someerror). It is just by convention that you shouldn't do that.
Which allows to unwrap nested errors by their type.
Doing it the stringly way often forces one to do string compare on the errors message. Which obviously is just bad.
In general the go error handling is plainly awful.
A common problem is, that functions always return something besides the error, which may or may be not a null pointer or random crap.
So one is free to ignore the error and then do other stuff with the return (like a later null check which might rely on undocumented behavior)
I really do not get it. Go has implicit interfaces but yet the most verbose and impractical error handling ever. Implicits like Scala! Making all the "go is explicit because it makes it easy to reason about and thus verbose error handling" arguments invalid.
Union types are a way better way to do proper error handling as proven by countless programming languages like rust and all the functional ones.
https://dev.to/lerno/a-new-error-handling-paradigm-for-c3-2g...
https://dev.to/lerno/more-on-error-handling-in-c3-3bee
Composing calls is much easier for one, plus there is quite a bit of syntactic sugar to make things even smoother.
Here how I think. There is a compiler? Put it to good use to help the developer instead of wallowing in "good practices" nonsense and "philosophies". Fact is, there is no error handling in Go (beside panics). It's all a convention.
Return values are a pair of an error value (internally basically an enum) and a result value. Error handling is mandatory, but passing errors to the caller from a nested function can be done by prefixing with `try`.
IIRC, Swift is similar.
Similarly, most languages (Java, Swift, etc) have generics, tuples, _and_ thrown exceptions. It's up to the user to determine whether to return a Result<T, E>, a (T, E), or throw an exception when an error occurs.
> which is opaque, hard to reason about, and can encourage some lazy programming habits.
In the same handwaving vein to me it is not opaque and does not encourage lazy programming habits. Having to return `err` is no different to raising an exception except that you do not do stack unwinding, but it does force you to write more code because the language designers have decided that dealing with every possible error, inline, using conditionals, is way more important to them than making the happy path of the algorithm readable as one unit of prose.
To me there is no difference in outcome when the happy path is written out first, and then the possible failure conditions are enumerated. It can be accomplished with sum types, but it can also be accomplished by matching on exception types.
The difficult part about Go's errors is that they are neither. For example, if you want a backtrace: congratulations, you have to use `errors` and wrap every single error you receive from your callees. You want to accumulate the causes of an error and pass it up? Well you better hope that your callees already have stored this metadata for you. The error is in `net/http/internalpkgoranother`? Bad luck, as it surely won't be wrapping for you. So some essential metadata is missing. Want to avoid ambiguous returns? Bad luck. If you read from an `io.Reader` and it returns you no error but the "read count" of 0, what do you do then? When you type match on the return value of the `io.Reader.Read()` and it tells you it read N bytes but it also gives you the EOF error - what do you handle first?
Even the basic Go idioms which are touted as great examples of composability and interface application (with which I can agree actually) have these edge cases of a double-meaning-return. Neither exceptions NOR sum types have this problem, so I guess both camps are unhappy.
What does put me off though is that the Go community seems to have this style of examining a (possibly arbitrary) Go design choice, and then to position it as THE way to write software. Sometimes it is done by denigrating developers who voice their stylistic disagreements with the choices designers have made ("you are not smart enough to use exceptions", "you are not smart enough to use sum types", "you are not smart enough to use generics"), and sometimes it is done with handwavy statements about code using other idioms being "opaque and hard to reason about".
I would really appreciate if the Go community could be a bit more mindful of when their praises of language designers sound to people preferring other tech like lecturing (or scolding, if you will). For instance because despite some very large projects using Go it might be alienating or "othering" a lot of people who do not share the enthusiasm or would otherwise use the language – and contribute to the ecosystem in a meaningful way – just for the sake of getting the job done.
var value = try fib(2) catch 'error'