92 comments

[ 0.72 ms ] story [ 153 ms ] thread
The whole point of error handling in go is that magic does not happen. Large error handling frameworks can create tracing difficulties. In the authors example he will never be confused as to where the error occurred. Is it overly verbose, yes. Do I prefer it yes. I am quickly able to identify and address issues.
well, no magic, goal reached.

However, the current implementation of error handling is atrocious.

If Go would take sum types and question marks from Rust, Go would keep the general error handling strategy and philosophy with the difference of being way less verbose and more ergonomic.

> If Go would take sum types and question mark

Go is an incomplete language masquerading as a simple one.

From “the rise of worse is better”:

> Completeness -- the design must cover as many important situations as is practical. All reasonably expected cases should be covered. Completeness can be sacrificed in favor of any other quality. In fact, completeness must be sacrificed whenever implementation simplicity is jeopardized. Consistency can be sacrificed to achieve completeness if simplicity is retained; especially worthless is consistency of interface.

I get that you don’t like some of the features go has done without, but how do you justify incomplete? What do you even mean by that word? Clearly lots of code has been written in it. Its deficiencies do not prevent programs from being expressed. The only definition I can think of for complete would be Turing completeness, but that is clearly too low a bar for what you are saying.
> but how do you justify incomplete

are you seriously suggesting that Go's insistence of requiring the programmer to scatter "if err != nil {}" after most function calls - with no compiler assistance if you forget - is complete either in the sense of "having all necessary parts, elements, or steps" or "being finished"?

it's terrible, causes bugs and it seems incredibly unlikely to me that it won't be improved.

Goland and Emacs (with Tree Sitter) can at least generate those automatically for you so they won't be forgotten, fwiw.
At very least, unless you hate other developers for some reason, you need to document in your test suite what errors are returned.

How, exactly, are going to forget a necessary `if err != nil` without your test suite blowing up?

If you do hate other developers for some reason, why not leave out some `err != nil`s to really drive home your hatred towards them?

If you can forget to check an error at a call site, then you can just as easily forget to add a proper test for that error condition. I prefer writing in languages where the syntax and standard library and compiler will help me avoid mistakes, and not have to rely on tests -- more error-prone code I have to write! -- to do that for me (and yes, I understand that I will never avoid having to write some tests, but the more the language and compiler can do for me, the better).

A language with generics and sum types (so you can implement something like Either, Try, or Result) doesn't let you use the result of a fallible function call without handling the error explicitly. And if the result is unit/void, most compilers/linters will still warn you if you haven't used the Either/Try/Result type that's returned.

But ultimately it doesn't matter. Write in whatever language you feel comfortable and most productive.

Well, unless you're starting a new security-sensitive project and want to write in C. Just... stop.

I don't follow. Errors are part of your functional specification. Not even a complete type system can avoid you needing to write tests to document the error conditions. We are not talking about writing tests to stand in where an incomplete type system may be lacking.

Assuming you don't hate other developers, and given the tests you would write in any language, how do you foresee missing an error branch? Again, not specifically trying to test for something that a sufficiently advanced type system would catch, just your regular level documenting that demonstrates to other developers that you don't hate them.

> then you can just as easily forget to add a proper test for that error condition

If an error condition is easily forgotten in the documentation, then who cares about the implementation thereof? It is obviously not important. In fact, it is arguable that your program is incorrect if you do handle the condition in such a case.

what tool do you use to verify that every possible path your program, including all errors paths in dependencies, is executed?
If you are referring to test coverage, the standard Go toolchain includes that out of the box.

But in this case you don't need to know about what paths have been executed. It will be obvious that an error path was not handled when your program does not adhere to function that is documented.

> including all errors paths in dependencies
No "compiler assistance" doesn't mean no assistance at all. There is a growing set of linters based on the community-accepted analysis package [0] that prevent this and other gotchas that Go devs may be prone to.

It isn't a perfect solution, but it works at community level without needing to wait for the Go team to take care of or changing the language/toolchain.

0: https://pkg.go.dev/golang.org/x/tools/go/analysis

> if you forget

Does this happen? Is there any data on how often error checking is forgotten?

In my experience, outright forgetting is very rare because an unused err should be a warning, but the following is not nearly as rare as it should be:

if err != nil { return nil }

With how boilerplate the correct version is, you learn to skip over it entirely, meaning it can be hard to spot that anything is wrong here. Linters do exist for this now, but they have so many false positives on other error handling cases that most people don't bother with them.

I think it's more a sign that Go will eventually pick up (some of/all of) the features people complain of it missing, in time. Maybe when it's a complete language it will still be simple; maybe trying to keep it artificially simple now will hamper it later. Remains to be seen.
I personally am not a fan of Go, but I agree that it's a fine language that people are productive in.

Opinions about languages are going to vary a lot, and I don't think it's productive to debate this kind of thing.

I prefer languages with stronger, more expressive type systems, but I'll freely admit that I code slower (though usually more correctly) in languages like that. Some people prefer languages where they can easily hold all the language's syntax and rules and quirks in their head, and fire out code quickly.

All of that is fine. There's no objective truth there. I agree with the GP that Go feels incomplete. Before generics, it felt embarrassingly incomplete. But if you're fine and happy and productive with it as it is, that's great!

Rust-the-language, maybe—but Rust-the-ecosystem makes heavy use of magic crates like `thiserror` and `anyhow`. I’m not saying it’s a better or worse philosophy, but it’s certainly a different philosophy.
thiserror and anyhow are hardly magic.

The former is just macros for error definition

The latter is just a wrapper around box dyn error

“Magic” isn’t a rigorously defined term, so let’s ignore it. Support for macros is a huge philosophical difference between Rust and Go.
I disagree in the context of error handling.

The go ecosystem frequently utilizes `go generate` to handle boilerplate code generation. That’s the exact same way `thiserror` utilizes macros to generate plain error structs. While macros can do more, in this case it’s the same.

thiserror and anyhow exist for ergonomic purposes (and enhance the experience of using the question-mark operator). Libraries shouldn't be leaking thiserror or anyhow types into their public interface. If they are, that's bad design.
It's not atrocious, when you work on Go code on a daily basis it's fine but I agree that it's repetitive.

Really an overblown problem from people that don't use Go much tbh. I work with people that used extensivly Java / C# / C++ and it's not the thing they complain about. They usually enjoy the fact that it's not exception based.

While sum types would be a welcome addition in general, summing an error is logically erroneous. It is an independent state.

The question mark is interesting, but as the try proposal discovered, how do you solve the leaking problem? That doesn't have a good solution yet.

> Summing an error is logically erroneous. It is an independent state.

Doesn't compute for me. Care to explain?

In practice it’s either-or in “error handling”, because that’s when you decide what to do and not do next.
Not true. As zero values are to be useful, one should never have to observe the error state unless the error is significant to the caller for some reason. Sometimes the error is significant. Sometimes it isn't. That depends on what the caller's needs are. Summing the error makes assumptions about the caller that may or may not be true. That is a bad API design.
Yes this is why you can just operate on the non error value (with map), immediately propagate the error (with ?), just operate on the error value (with map_err), or ignore it completely (eg by converting it to an option or using something like unwrap_or_else). Errors in Rust are still just possibly present values: they’re just wrapped in an interface (a sum type) that forces you to be explicit about your intent to ignore, handle, or propagate the error.
Propagation suffers from the leaking problem. Rust has solutions for that, but it's not clear how that translates to Go, hence why the "try" proposal failed. I'm sure there is some solution out there, but nobody has come up with a good one. It turns out if nobody does the work, the work doesn't get done.

Again, needing to be explicit about your intent to do anything with an error is faulty. The values are not dependent. Just because some languages have gotten it wrong doesn't mean they all should.

What you're saying doesn't make sense to me, and I also don't know what you're referring to by the leaking problem. I don't see how the choice of being explicit or non-explicit about errors is "faulty." Plenty of languages choose to use exceptions and maximize your ability to ignore errors, while others force you to deal with (in some way) the fact that an operation is fallible. Neither of these is "wrong," in my opinion, but they do offer different tradeoffs and ergonomics.

I'm not sure why you say that the values of a sum type must be "dependent," or what you mean by that. Maybe/Option is a sum type of Nothing or Some<T>. There's no dependency there. If my webserver can return JSON or XML, I might represent that with a sum type holding either a JSON object or an XML tree, but there's no dependency between those two things that I can see.

I really appreciate the lack of magic in go's error handling. I do not appreciate the specific implementation of errors in go. It's inefficient (`reflect` is all over the place) and prone to obscuring the actual "error path". In applications of non-trivial size, it also becomes an obstacle to implementing structured logging and other components of observability. This makes debugging/triage of, say, a production incident harder than it needs to be.
I've never seen or used reflection with error handling in Go, what do you mean?
But this is a detail of implementation, user code does not use reflection with errors.
That "detail of implementation" is what is used by user code, e.g., `myError.As(...)` uses reflection, implicitly.
Then what? It has 0 performance impact, we're talking about error handling here, it's rare and not in hot path.
It does not have 0 performance impact, it's not rare, and in applications that are in the hot path, error handling is also in the hot path.
No, it only executes when you enter the err != nil block so it's almost never called, again 0 impact on performance.
I wouldn't classify type assertion as reflection. It's a super fast type check of the object header, not dynamic reflection.
in 8 years of writing go at scale i've never used reflection in production code. gonna need an example of what you mean.
(comment deleted)
I like the way this pattern trends, but one downside is that it mixes HTTP concerns into the service layer. E.g. the service layer needs to know exactly which HTTP error code this responds to when returning an error.

If the service layer is used for something other than a web server, e.g. a handler for a queue, we've now got a kinda funky abstraction.

I notice that the HTTP Request and ResponseWriter is also mixed into the service layer, which is another thing I would probably move one step closer to the actual HTTP server and away from the logic that interacts with the database.

In the past I've used `Temporary()` or `Permanent()` along with some other additional fields to help map to HTTP status codes at the HTTP server's handler layer.

I've done interfaces where centered around the kind of error that happened, which is typically standard across apps and translates well into status codes for http or grpc.

Eg "NotFound()" "InvalidParameter()" "Unknown()".

That said these could all just be sentinel errors with the normal unwrapping in the stdlib these days.

This is def an anti-pattern.

As soon as you have your services suddenly hooked up to something else - chron (or hangfire or whatever) jobs or a message bus - it gets a bit weird because your service layer thinks you're still responding to an HTTP request and whoever works on the service now has to think about HTTP as well which is pretty fucking bizarre to begin with.

And your unit tests for a business logic service method are gonna look at HTTP errors? It feels dirty, but not in a fun tap-your-nose way.

It's really not hard to just return errors relating to what actually happened (can't find user by id, user is blocked from logging in, whatever) and have the app's ingress points (API controllers, message handlers, job runners) decide how to surface that in a way that makes sense for the specific interface.

I agree with you in general, and I've never written code in this style.

But at the same time, converting between error and result domains is tedious and error-prone. As much as we believe separating concerns is a good thing, it's pretty likely that service layer code will never ever be used outside a HTTP service context, so... so what?

I probably still wouldn't design like this in my own stuff, but I'm not convinced it's actually so terrible.

I guess it really depends on what kind of stuff we work on. It's very common for service layers in systems that I work on to be shared between various ingress points (HTTP, GRPC, Kafka handler, etc).
Post author here.

For any non-trivial app, there would ideally be a set of domain error types/codes that the various application layers know about, rather than concerning themselves with HTTP statuses.

I didn't discuss that in the article, because I felt like it was a bit tangential. Perhaps it was a mistake to omit that detail.

In this example, you have to magically know that DB returns a statusError{} in order not to wrap it again.

I've found one improvement is to encode this knowledge in the type system, by not using plain error returns on the handler, but instead use something like e.g.

    type netError interface {
        Error() string
        Public() bool
        StatusCode() int
        Unwrap() error
    }
    
    func PublicError(e error, code int) netError { ... }
    
    func InternalError(e error) netError { ... }
    
    func SomeHandler(h *http.Request, w http.ReponseWriter) netError { ... }
This makes it clearer whether the underlying err "knows" if it should be public or private.
This seems like a good idea, but it will break lots of things. In particular, err != nil will break. Don’t do it.
err != nil works fine, exactly the same way it works for plain error: interface values can be nil,
(comment deleted)
Returning interfaces is an antipattern masquerading as a design pattern... Just saying
https://github.com/augustoroman/sandwich is another approach for achieving a similar result. One nice aspect of writing handlers that return errors is that they are easy to test and clean to write.

Sandwich does very simple sequencing to achieve an understandable dependency injection mechanism that produces clear errors when something goes wrong, and it produces errors at setup time rather than run time.

Interesting. The DI can also be done with something like UberFX.
(comment deleted)
In general, the way you output errors to the client and the errors you log are two totally different things. This is a slight tweak from the problem the author is attempting to solve by letting the client see the error.

Like the author, I created a special error interface I can look for when returning client errors: https://github.com/Xeoncross/public-error-go

(comment deleted)
Did anyone else immediate get confused by the assertion that the http code shouldn't know about the database details so the solution is for the database code to embed http details instead?

In our project we have a domain types package with errors that permeate these types of boundaries. It's "okay" in the ways everything in Go is "okay". I've yet to see anything I really like in this space.

Yeah, you should have an error domain and convert a sql.NotFound error to an app.NotFound error at the boundary. The http handler can then treat those as 404s.
(comment deleted)
Yes. In fact, I am continually confused by how as soon as developers see the word "error" they forget how to write software. They can be perfectly competent developers when handling any other type, but as soon as "error" shows up on the screen all bets are off. It is bizarre.

Developers as a rule also seem to act strange if you suggest that they made an error. Perhaps the word "error" is triggering in some deep-seated emotional way? I wonder if, in software, we called errors "pineapples" instead if it would avoid triggering such emotions, allowing one to reason about errors in the same rational way they do all other values?

Post author here.

I probably could have made this a bit more clear, but I feel it was a slight tangent to the main point of the post: In any serious app, I wouldn't actually set an HTTP status directly in the db layer. Rather, I would do what others in this thread have suggested: I'd have a set of domain error types/codes, and my DB layer would set _that_.

That domain error value would then implement the `HTTPStatus()` method to return the appropriate status code.

This allows you to have multiple "not found" error codes (as an example) for internal use, which all resolve to an HTTP status of 404.

I agree with this. I usually wrap my HTTP handlers in a function that copies the error to the HTTP response, and can then just return an error from my actual handler.

I think it's fair to not force this in the standard library. HTTP doesn't have the concept of errors; just a status code, headers, and body (ok ok, and 93 other fun things you can do; chunked responses, upgrades, etc.). If you want to write the wrapper, write the wrapper. If you don't, don't.

Sounds like the author is missing an interface to abstract the DB backend.
Distilled, it’s the wet dream of mapping errors/exceptions to http codes. I just decided that 200 ok, 400 you, 500 me is enough for non-file and/or non-browser-itself-requested-it requests. And I like much more when API docs don’t require me to set switch-cases around their return codes, because it all ends up in a single “if not 200, log status and body and throw/return '{you|me} failed'”.

Moreover, I’ve seen things returning e.g. an array of >0 orders with 200 and an empty array with 404. This makes me emotional and breaks HN guidelines.

Just a tiny bit of sugar would go a long way silencing 50% of the complains about error handling in go...

Not an error handling improvement per-se, but if we had to sprinkle just one line of code instead of three around all spots of error handling, that would make me so much happier :-)

From:

    if err != nil {
      return err
    }
To:

    return err if err != nil
... that is, allow ruby-style placing of conditionals after the return.
In these cases I'll almost always name the output error parameter.

    func x() (err error)
The short variable declaration syntax only requires that _at least one_ name is unique, so you can freely use the variable even in := sequences. So you end up with something like:

    if thing, err := impl(); err != nil {
        return
    } else {
        // use thing
    }
In the articles first example, the "problem" is that you have a core of a method that can return error or a widgetJSON. There should be a separate function that does precisely that. If you want to mash your http logic and your use logic into the same function, you're going to have a mess no matter how hard you try.
What I'm suggesting would make this snippet nicer too:

    return err if thing, err := impl(); err != nil

    // use thing
It would remove the indent, and plus you wouldn't need to name your return variable, that can sometimes be error prone (say, if you end up returning the wrong thing). Just a lil bit of sugar!
I might be in the minority here, but IMO that snippet does not look nicer and looks pretty ugly. You've got like 3 things going on all jumbled together.

I would not want to encounter that line of code in the wild, or 3 months after writing it, and attempt to parse what it's doing.

Obviously in real code you need to return a new error each time, not simply pass on the error you received. In actual usage, it seems that one line is going to become painfully long, or span multiple lines in some way. Do you still think that is an improvement once you move past this contrived example?
So two things:

* in the wild, a LOT of code has the archetypical 3 lines: if err != nil { return err }, so I'm suggesting a lil sugar will remove this noise from countless lines of code across a lot of projects.

* If more processing is needed (ex: wrapping the error), use a function (return Wrap(err, ...), etc) or just revert to the normal if syntax.

> in the wild, a LOT of code has the archetypical 3 lines: if err != nil { return err }

The data collected during the "try" proposal evaluation told that is not true. It was occasionally found, but not all that commonly. Maybe there is "a LOT" if you are counting comments with made up example code on HN? But as far as actual codebases that are compiled and run go...

There is also a concern, if included just because, that it would encourage developers to start using `return err` where they shouldn't. Which, as you know, would lead to the errors becoming unusable.

> If more processing is needed

How about some concrete examples and how you see them being formatted? You might have a good idea here, but it's not clear what it would look like in something real.

> or just revert to the normal if syntax.

If it is only useful for `return err`, why bother? That would be like adding special loop syntax that is only for counting from 0-9. Yeah, maybe once in a blue moon you really will count from 0-9 and it'll be cool on that day, but in reality it's not general enough to warrant inclusion in a language.

That said, I do like that your idea does not need to be limited to errors. A lot of error-related ideas are quite flawed in that they only work for errors. In reality, all of the problems with errors in Go are also problems for every other type. The right solution to improve upon those problems will equally solve for email addresses and birthdays.

re: "a lot", I don't have hard data but it is what I see in my own code, code I use and even whenever someone needs to post a lil snippet of code on videos, blog posts, etc. I just feel those three lines are pure friction for no good reason.

I actually like the suggestion below, that would involve just a little, backward compatible, tweak on how fmt works [1].

1: 1 hour ago

tangent: does HN replace links to specific comments like this? "1: 1 hour ago"? I've never seen that, I thought I linked to the specific comment 「(・ิ.・ิ)
this is so dumb... I think copied the text instead of copying the link source lol
I hope you realize that this won’t even compile.

The err in your if is a newly allocated variable by :=, different than the err allocated by the x prototype, and you’d also return a nil error even if impl returned an error.

The Go compiler is smart enough to prevent you from doing this beginner mistake:

result parameter err not in scope at return. inner declaration of var err error.

https://go.dev/play/p/VdWVOL51wZf

You would have to first declare thing prior to the if statement, then replace := by = .. the beauty of adding verbosity and longer-lived variables for the sake of trying to write return instead of return err in Go.

I wrote it that way because that was I was looking at in the example code. You're mostly right. The example from my own code, of course, looks more like this:

https://go.dev/play/p/qdjnH3JhzTN

Which does work because the rules on := are loose, but the if captured version creates a new scope that can't be shared with the return variable.

Formatting the if onto a single line gets you most of the way there.

    if err != nil { return err }
Not a bad idea actually! I would be happy with this too! It would require changing go's fmt to allow inline code in braces if it consists of a single return... that would be backward compatible with other code too (just make fmt not revert the previous style to a single line).
I've started playing around with using the ResponseRecorder from httptest to create a middleware that records the response from a handler and then deals with it appropriately:

- if it's an error then display an actual error page (or redirect to login, or whatever) - if it's 200 OK then minimise the logging to just "successfully processed request for GET /foo?bar"

I like OP's way of dealing with the "http.Error then return" problem, especially since I usually find I'm also logging the error (so slog.Error, then http.Error, then return, which is painful since I'm repeating myself for the Error calls). But I don't like messing with the http.Handler interface. I think there's an approach involving both - like creating a type that implements http.Handler and handles logging, error reporting and error redirection while still allowing the core function to just "return err". I'm going to experiment with this. Thanks for the thought OP :)

We just need an option type, thats it
There's so much contention over error handling in Go. Over the years I've settled on treating it like gofmt: I may not 100% agree with all the choices, but it is what it is so let's just get on with it.

And to be honest as a majority-C# dev I quite like the break from exception handling.

As an aside, one thing I've found which makes life easier is to invert the check. With convoluted logic it can lead to excessive nesting, but then nobody would write code that did enough in a single function to make that an issue, surely?

Practical example. Here's what you might normally see:

  err := doFunctionA()
  if err != nil {
    handleErrorFromFunctionA()
  }
  err = doFunctionB()
  if err != nil {
    handleErrorFromFunctionB()
  }
  err = doFunctionC()
  if err != nil {
    handleErrorFromFunctionC()
  }
I'd usually just switch it:

  err := doFunctionA()
  if err == nil {
    err = doFunctionB()
    if err == nil {
      err = doFunctionC()
    }
  }
  if err != nil {
    handleErrorRegardlessOfSource()
  }
Of course it only works if the errors can be sensibly handled the same way regardless of their source, but that's surprisingly often the case (eg log, display, and/or return it).

It's just a minor change but it's standard uncomplicated Go, and with multiple sequential operations it reduces the code-waffle even more the larger the code gets.

YMMV and it isn't always suitable, but every little helps.