It’s great that they identified this (incredibly common) pain point and introduced a way to solve it, but I can’t help being disappointed.
Reading the examples I found myself thinking, “that looks like a really useful pattern, I should bookmark this so I can adopt it whenever I write code like that.”
The fact that I’m considering bookmarking a blog post about complex boilerplate that I would want to use 100% of the times when it’s applicable is a huge red flag and is exactly why people complain about Go.
It feels like you’re constantly fighting the language: having to add error handling boilerplate everywhere and having to pass contexts everywhere (more boilerplate). This is the intersection of those two annoyances so it feels especially annoying (particularly given the nuances/footguns the author describes).
They say the point is that Go forces you to handle errors but 99% of the time that means just returning the error after possibly wrapping it. After a decade of writing Go I still don’t have a good rule of thumb for when I should wrap an error with more info or return it as-is.
I hope someday they make another attempt at a Go 2.0.
I actually like the explicit error and context value stuff in Go, though I recognise I'm in the minority.
The main reason is more to do with maintaining Go code than writing it: I find it very helpful when reading Go code and debugging it, to see actual containers of values get passed around.
Also, whenever I write a bit of boilerplate to return an error up, that is a reminder to consider the failure paths of that call.
Finally, I like the style of having a very clear control flow. I prefer to see the state getting passed in and returned back, rather than "hidden away".
I know that there are other approaches to having clear error values, like an encapsulated return value, and I like that approach as well - but there is also virtue in having simple values. And yes there are definitely footguns due to historical design choices, but the Go language server is pretty good at flagging those, and it is the stubborn commitment to maintaining the major API V1 that makes the language server actually reliable to use (my experience working with Elixir's language server has been quite different, for example).
Golang returning tuples but not having pattern matching is something I'll never get. I really feel it's a too dumbed down version of erlang/elixir, especially with this context passing business
I think this post needs better examples to show case the issue, because right now the issue is not clear. Ideally you would need an example that uses the context.Cause function, see below
The contexts and errors communicate information in different directions. Errors let upstream function know what happened within the call, context lets downstream functions know what happened elsewhere in the system. As a consequence there isn't much point to cancel the context and return the error right away if there isn't anybody else listening to it.
Also, context can be chained by definition. If you need to be able to cancel the context with a cause or cancel it with a timeout, you can just make two context and use them.
Not only that, isn't this a "lie"? You're cancelling the context explicitly, but that's not necessary is it? Because at the moment the above call fails, the called-into functions might not have cancelled the context. There might be cleanup running later on which will then refuse to run on this eagerly cancelled context. There is no need to cancel this eagerly.
Perhaps I'm not seeing the problem being solved, but bog-standard `return err` with "lazy" context cancellation (in a top-level `defer cancel()`), or eager (in a leaf I/O goroutine) seems to carry similar functionality. Stacking both with ~identical information seems redundant.
12 comments
[ 3.4 ms ] story [ 28.1 ms ] threadIs there any equivalent in major popular languages like Python, Java, or JS of this?
Reading the examples I found myself thinking, “that looks like a really useful pattern, I should bookmark this so I can adopt it whenever I write code like that.”
The fact that I’m considering bookmarking a blog post about complex boilerplate that I would want to use 100% of the times when it’s applicable is a huge red flag and is exactly why people complain about Go.
It feels like you’re constantly fighting the language: having to add error handling boilerplate everywhere and having to pass contexts everywhere (more boilerplate). This is the intersection of those two annoyances so it feels especially annoying (particularly given the nuances/footguns the author describes).
They say the point is that Go forces you to handle errors but 99% of the time that means just returning the error after possibly wrapping it. After a decade of writing Go I still don’t have a good rule of thumb for when I should wrap an error with more info or return it as-is.
I hope someday they make another attempt at a Go 2.0.
The main reason is more to do with maintaining Go code than writing it: I find it very helpful when reading Go code and debugging it, to see actual containers of values get passed around.
Also, whenever I write a bit of boilerplate to return an error up, that is a reminder to consider the failure paths of that call.
Finally, I like the style of having a very clear control flow. I prefer to see the state getting passed in and returned back, rather than "hidden away".
I know that there are other approaches to having clear error values, like an encapsulated return value, and I like that approach as well - but there is also virtue in having simple values. And yes there are definitely footguns due to historical design choices, but the Go language server is pretty good at flagging those, and it is the stubborn commitment to maintaining the major API V1 that makes the language server actually reliable to use (my experience working with Elixir's language server has been quite different, for example).
The code that justifies the special context handling:
Why not simply wrap that error with the same information?The contexts and errors communicate information in different directions. Errors let upstream function know what happened within the call, context lets downstream functions know what happened elsewhere in the system. As a consequence there isn't much point to cancel the context and return the error right away if there isn't anybody else listening to it.
Also, context can be chained by definition. If you need to be able to cancel the context with a cause or cancel it with a timeout, you can just make two context and use them.
Example that shows the approach as well as the specific issue raised by the post: https://go.dev/play/p/rpmqWJFQE05
Thanks for the post though! Made me think about contexts usage more
Perhaps I'm not seeing the problem being solved, but bog-standard `return err` with "lazy" context cancellation (in a top-level `defer cancel()`), or eager (in a leaf I/O goroutine) seems to carry similar functionality. Stacking both with ~identical information seems redundant.
> lemoncucumber
> it can be tempting to keep adding layer upon layer of wrapping resulting in an unwieldy error string that’s practically a hand-rolled stacktrace
I thought this was the whole reason to wrap errors, to know where they passed up the chain.
Funny how it seems no matter the subject, if Go is involved, errors get discussed.