Hacker News new | past | comments | ask | show | jobs | submit login
Why Go's Error Handling Is Awesome (rauljordan.com)
61 points by michaelanckaert on July 7, 2020 | hide | past | favorite | 93 comments



Go error handling is barely better than C error codes.

   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.


That's not exactly what Go does, as you can return much more information and it's almost always clear what's an error and what isn't (there are a few exceptions, such as strings.Index() returning -1).


> That's not exactly what Go does, as you can return much more information and it's almost always clear what's an error and what isn't (there are a few exceptions, such as strings.Index() returning -1).

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".


strings.Index() => -1 is not an error, but a result, ie. empty result is not an error. A more sophisticated type system would provide more differentiation, but that also have costs.

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.


Go errors are a specific type and that type is extensible. You can attach additional data to an error, and in recent versions of go you can wrap errors in other errors. You can do instance checking against an errors.New error, or do a type check against the kind of error you get back. This ultimately gives you significantly greater control and functionality over error codes.


Go errors are a convention. Nothing forces you to use error interface to do anything nor handle anything either.

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.


You could do much of that with C structs as well.


You can get all these benefits and not have any of the downsides if you have something like Result or Either, which requires generics. You only really compare Go's solution against exceptions. That's not the only solution that exists.


...or pattern matching in case of dynamically-typed languages:

  case some_operation() do
    {:ok, result} -> # do something with result
    {:error, error} -> # handle the error
  end


You lose the ability to make sure you've handled an error AOT, but sure, this feature is pretty much the same as a sum type except that it's structural. When I used to write typescript, there were plenty of times I was happy to have it's union types!


Go's error handling is, frankly, terrible. It being better than some of the competition doesn't mean it's good.

It'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...


Isn't Rust's error handling essentially the same as Go's except that Rust has a bit of syntax sugar built around it? In Rust you still have a composite return value which bundles an error code with a value-payload, it's just wrapped in a Result<> type. So the whole thing is also just a convention, just as in Go, but built around Rust's language features.

(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).


For a Result<T, E> the returned type will either be an Ok(T) or an Err(E). Not both, so it's not a composite type like golang with defining a Tuple of (T, error). So you can then do a match statement like this:

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.

[0]: https://doc.rust-lang.org/std/result/index.html


Ok, this requires Rust's "rich enum type" (whatever that's called), and some sort of generics, both of which Go doesn't have.

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.


>"rich enum type" (whatever that's called)

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.


> 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.


(They're called 'enums' in Rust though, your parent is correct.)


> Ok, this requires Rust's "rich enum type" (whatever that's called), and some sort of generics, both of which Go doesn't have.

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.


> This is one of the reasons so many people are calling for Go to get generics.

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".


I'd say that's perhaps too broad of an interpretation of 'syntax sugar' though.


Reading this, I was visualizing in my head an article titled Why Nulls Are Good which started with "Look, you may not like the idea of values that may or may not be present, but the truth is that a lot of programming relies on it. Why imagine..." and going on and on about optional values and never once addressing the real accusation about null, which is that it's a stupid way of accomplishing optional values.

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.


oddly, the feature that is suggested as being awesome is in fact go's _worst_ feature, which is:

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..).


I was about to write the same thing. If your argument about how something is an awesome feature relies on "and most linters or IDEs will likely catch when you missed something or did it wrong" you might not have such a great feature.


Go communicates errors, and you can do something those errors (i.e. handle them). Ergo, it has error handling. It may not be the kind of error handling you like – which is perfectly reasonable – but that's not the same as Go not having error handling.


> communicates errors, and you can do something those errors

By this definition is there a language that doesn't have "error handling?"


Following this, assembly too has error handling.


The vast majority of my error handling is for failures you really can't do anything about locally (e.g. network connection died, couldn't connect to database, couldn't write to file system) so a few try/catches close to the top-level of the program (instead of strewed across all your files) are pretty ideal for me when working on e.g. frontend, games, simple backend APIs.

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.


1. In Java you have both checked and unchecked exceptions so you have the option to force the developer to handle the error explicitly or not. With Go there is no choice.

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 don't know how this is better. Just looking at the signature I don't believe this is more powerful or elegant than the same equivalent in Java: - what exactly the object is doing with your error, I assume this is an IO monad so no changes are applied on the right side after an error but to be sure I would have to look at the library doc and rely on it rather than having this behavior enforced as part of the language design. - it doesn't require you to handle the exception or be clear that you handled it, for 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. 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 strictly about your example but in Scala there's no standard way to handle exceptions, so if you're required to integrate this code with a library outside ZIO that uses a different pattern (java exceptions, Either, value classes, etc) it would start to feel you're writing a lot of glue code that should be part of the language design itself.

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.


IO is a monad, so you get pretty nifty composition out-of-the-box already with Scala. Since it's ZIO, you get even nicer error handling that you would with, say, cats' IO, where you i.e. have to deal with nested Monads and need to use Monad transformers, but that's another story.

> 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,...).


> Not quite sure what you mean by that. Care to rephrase? It should be hard or impossible to produce code that doesn't catch an exception or ignore it and just bubble the exception up on the call stack. The consequence of it is that sometimes it's easy to have a low level exception arriving on your presentation layer, which totally breaks encapsulation.

> 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)


Go's error handling really reminds me of errbacks in node (callback function with a possible error as first argument), with many of the same pitfalls. I wonder why so many people defend Go's choices here in comparison?


Go is aiming to be a slightly better C. If you look at it from a C programmer's perspective then Go streamlines the typical C control flow of error values by letting you return a value and an error code.

It's not trying to revolutionise the world.


"errbacks" definitely have benefits since having the error parameter forces you to think "where should I handle this error". With exceptions, people often get sloppy and let them be thrown and handled wherever, often at the wrong place.

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


The benchmarks in that library are misleading - by wrapping the resolve in the promise example in a process.nextTick you're actually making the engine enqueue 2 microtasks, as promises are always guaranteed to be async. If you remove the process.nextTick the results are reversed, I get:

- 63ms (async)

- 100ms (casync)

so it's almost twice as slow as native async await in this example.


The main reason for this library has to do with simplification more than performance but just for the sake of argument, in my little benchmark, the point was to make the asynchronous operation identical in both tests. You can use promises without any asynchronous operation inside but what's the point? That's not how they're used most of the time. And sure promises may add a second microtask around the asynchronous code (casync does too in some cases) but that is to their detriment performance wise (and do promises really add a second microtask when the code inside is already asynchronous?).

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 agree with the article. First I had to get used to it, and now I like it. 1) It forces a programmer to think about all possible errors right when calling a method, and handle them, and 2) reading other people code is easier - I see right away which errors can happen exactly and how they were handled.

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.


The author's arguments apply better to demonstrating that `Maybe` is good, in my book. It's conceptually cleaner to pattern-match on the optional `err` using a fully general `match` construct that works on arbitrary discriminated unions, than using an `if` statement, as far as I'm concerned.


These discussions are always frustrating, because it feels like the people advocating Go's approach are comparing them to Java style Checked Exceptions, while the people most annoyed with Go's approach are usually comparing them to Sum Types (like Rust's Result or Haskell's Maybe/Either).

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.


I agree but with a caveat.

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.


IMHO your overall thesis of "Go and Rust are very similar here" is correct, but you're missing the largest difference, IMHO. In my mind, Go and Rust's mechanisms are very close, except that:

* 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.


Thank you, this is an important distinction and informs the different structural idioms of handling these cases:

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.)


> The Rust and Go error handling approach is only superficially different.

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...


> in the Rust world it's understood that the Rust and checked exception error handling approaches are only superficially different

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.


Ironically, Go's error handling is part of why I'm rewriting an application in PHP that I spent two years writing in Go. Not a big part, but a part.


Care to elaborate on why it was such an issue?


Partly, it cluttered the code and decreased legibility.

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.


This is a really underrated take on the issue. These “simple” errors become a huge issue when you are trying to track them down. There are no codes or namespaces so you end up having to be really diligent about how you log and wrap them. This can be ok for small projects but once you’ve got a team working on things it can get really frustrating getting everyone on board.


I find C#/ASP.NET Core to be the perfect middle ground. You get very high performance† (at least for such a high level framework), clear error messages, nice debugging, arguably better type system than both of those, and so on. It's fully cross-platform these days, I don't touch Windows at all.

https://www.techempower.com/benchmarks/#section=data-r19&hw=...


I agree, the lack of stack traces can be annoying when debugging Go programs. Instead of returning raw errors everywhere, the problem can be alleviated by adding some context to returned errors:

  if err := foo(x); err != nil {
      return fmt.Errorf("foo: bad argument %s", x)
  }


The problem with this is, that the caller of the function which returns what you wrote (i.e. a dynamic error message) can't match the error anymore for conditional error handling. That's because errors in go are just strings. I guess a solution could be to provide an error matching function but that seems quite cumbersome compared to typed errors.


This problem has been addressed in Go 1.13 with wrapped errors. If you get an error from a filesystem operation, you can

  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.


You should never match against "err.Error()" for conditional error handling, a better solution is to use a custom error type. There are a bunch of different ways to approach this in Go.


Ah that's better, thanks. here a link for others who are reading this https://gobyexample.com/errors


Except most libraries do nothing of the sort, so you end up having to do a substring search.


I've rarely run in to this, but if you do then I'd expect most people will be happy to accept a PR.


So re-invent exceptions, but in an inferior way.


You can add stack traces to errors in Go with a few lines of code; there are many existing 'errors' packages which do that. You can also send this to Sentry.

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).


>Most linters or IDEs will catch that you’re ignoring an error, and it will certaintly be visible to your teammates during code review.

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)


For me error handling in go was one of the biggest downsides. Using ADTs and union types for Either is most pleasurable error handling case I've encountered.


Handling errors as values is indeed nice. While exceptions can be convenient in some cases, I find that the out-of-band control flow is over-complicated and more of than not leads to poorly documented error paths that are treated as an afterthought.

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.


IMNERHO, the one significant misstep in checked exception based error handling is that languages don't (AFAIK!) require call sites to be marked. If a function is marked as throwing an exception, and it's called from another function marked as throwing the same exception, that's enough. There's no indication to the reader that an exception can be thrown at a particular point, much less the the author was aware of the fact that it can!

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.


> 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.


Swift essentially does that, but uses a try keyword, not something that looks like a function. See https://docs.swift.org/swift-book/LanguageGuide/ErrorHandlin....


Well, in Java's defense, at least you don't forget to check the error like you could in Go.


While I'm no great fan of Go's error handling, I've not found this to be an issue. The compiler yells at you if you forget. You'd have to explicitly ignore the error by underscoring it to "forget" it.


No it doesn't:

    err := foo()
    if err != nil {...}
    err = bar() // accidentally ignored
    err = baz()
    if err != nil {...}


How do you propose handling an OOB index in an array?


I follow Joe Duffy in recognising that we should make a distinction between "errors" and "bugs" [0]. IOException is an error; out of bounds access, division by zero, assertion failures, and things like ClassCastException and NullPointerException in Java, are bugs.

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


But once you've accepted unchecked exceptions (and paid the cost of the having them), it just doesn't make sense to only use them only for bugs. For example, it is better to

  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 idea is that for bugs you don't even need a try-catch (or maybe one around your entire program). And pattern matching and functions like `map` make the second case much nicer.


Error handling is one of those things that involve a lot of nuance, and that most people try to turn into a black-and-white argument.

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.


If all functions return a possible error, how do you return a value OR an error?


You can't which is a big part of the problem. You have to trust that the function will only return either and not both. Except in cases where a function does return both.

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.


A "maybe" type is logically incompatible with "subatomic I/O". When a sized-read() syscall results in a short read AND a raised error. The only sensible thing to do is to return both.

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...?


That's an API choice. There's no technical reason at all why you couldn't return some kind of error AND read data if you wanted, you'd just have to make your Error type a product type.


That’s hardly a reason not to include a maybe type. In that particular case, instead of a maybe you can use a combination of sum and product types or just an additional ShortRead type.

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.


You could write sum type that handles multiple cases.

Value Error ValueAndError


I like Rust's approach of using `Result` enum


You don't, at least not usually.

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.


The idiom is to return two values: the desired return value, and also an error value. Go has support for multiple return values, and easy syntax for assigning those positional return values to variables at the call site.


Can't you simply return a nil error in case you want to return a value? Check for error and if nothing bad happened use the value.


There is nothing stopping you from returning both an error and a value. Sure, you can just be diligent everywhere and not do that, we know how well that type of advice usually turns out.


The author seems not to be aware, that there is a "better" version of fmt.Errorf using %w instead of %v since go 1.13.

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.


As alternative, I introduced “failables” into my programming language C3. It has the benefits of Go error handling but none of the downsides. I wrote two blog posts as I developed the idea:

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.


There are other ways of handling errors besides exceptions.


The question is, Is the author of that post aware of that at first place. Or is he building an disingenuous argument by ignoring other forms of error handling beside exceptions, in order to make Go "convention" look good?

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.


Judging from the way the author presented the info, I think they are clearly unaware to there being other methods of handling errors other than exceptions.


I think Zig's error handling is even better: https://ziglang.org/#toc-A-fresh-take-on-error-handling

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.


This is a similar argument as for functional programming: sure it's great that functional programming has pure functions, but most languages allow for pure functions _and_ for procedural code.

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.


I find it hard to reason about text which contains stylistic assumptions which are quite handwavy, such as

> 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.


try catch can provide the best of both worlds if your language allows statements as values

  var value = try fib(2) catch 'error'


It seems many people praising golang's error handling have yet to use it in large production projects. It's absolutely horrid dealing with it.




Join us for AI Startup School this June 16-17 in San Francisco!

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: