Error handling in Go web apps shouldn't be so awkward
boldlygo.tech
boldlygo.tech
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.
Go is an incomplete language masquerading as a simple one.
> 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.
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.
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?
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.
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.
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.
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.
Does this happen? Is there any data on how often error checking is forgotten?
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.
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!
The former is just macros for error definition
The latter is just a wrapper around box dyn error
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.
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.
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.
Doesn't compute for me. Care to explain?
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.
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.
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.
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?
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'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.https://github.com/caddyserver/caddy/blob/cb86319bd50322d4ac...
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.
Eg "NotFound()" "InvalidParameter()" "Unknown()".
That said these could all just be sentinel errors with the normal unwrapping in the stdlib these days.
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.
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.
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.
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.
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.
Like the author, I created a special error interface I can look for when returning client errors: https://github.com/Xeoncross/public-error-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. 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. 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 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.
* 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.
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.
I actually like the suggestion below, that would involve just a little, backward compatible, tweak on how fmt works [1].
1: 1 hour ago
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.
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.
if err != nil { return err }https://github.com/golang/vscode-go/issues/2311
It papers over the issue, but helps some.
- 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 :)
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.
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.