Good lord, how is it elegant to need to turn your code inside-out to accomplish the basic error handling available in pretty much every other comparable language from the last two decades? So much of Go is well-marketed Stockholm Syndrome.
I don’t think that responding with a 404 if there are no rows in the database is that any language supports out of the box. Some frameworks do, and they all have code similar to this for it.
And sadly so often error handling is often done in a poor manner in the name of abstraction, though really bad one that effectively boils down to ignore that things can go wrong, meaning that one ends up digging through many layers when they actually do go wrong.
People eventually give up and copy paste StackOverflow[1] solutions in the hopes that one of them will work, even when the fix is more accidental and doesn’t fix the root cause.
The pinnacle was once checking code that supposedly could not fail. The reason was that every statement was wrapped in a try with an empty catch.
But back to the topic. Out of the box is all good and nice until you want to do something different which in my experience happens more often than one would think. People sometimes create workarounds. In the example of non existing rows, for example doing a count before fetch, so doing two queries instead of one, just to avoid cases where a no rows error would otherwise be thrown.
Now i am certainly not against (good) abstractions or automation, but seeing people fighting against those in many instances makes me prefer systems where they can be easily be added and can easily be reasoned about, like in this example.
[1] Nothing against StackOverflow, just blindly copy pasting things, one doesn’t even bother to understand.
It is possible to improve the state of the art while also having a language like Go that is practical, compiles unusually fast and is designed specifically to solve what Google found problematic with their larger C++ projects.
There is nothing unusual about it. It’s only C++ and Rust that are slow to compile. Pascal, OCaml, Zig and the upcoming Jai are decent. It’s not that Go is incredible, it’s that C++ is really terrible in this regard (not a single, but a lot of different language design decisions made it this way).
For single files, I agree. But outright disallowing unused dependencies, and designing the language so that it can be parsed in a single pass, really helps for larger projects. I agree on Zig and maybe Pascal too, but in my experience, OCaml projects can be slow to compile.
My impression from hanging out in #zig is that the stage 1 compiler is known to be slow and inefficient, and is intended as a stepping-stone to the stage 2 compiler, which is shaping up to be a lot faster and more efficient.
Also there’s the in-place binary patching that would allow for very fast incremental debug builds, if it pans out.
My experience with Go is that it’s actually very slow to compile. A whole project clean build might be unusually fast, but it’s not so fast that the build takes an insignificant amount of time; it’s just better than many other languages. An incremental build, however, is slower than in most other languages I use; my C++ and C build/run/modify cycle is usually significantly faster than in Go, because its incremental builds are less precise.
In Go, incremental builds are on the package level, not the source level. A package is recompiled when either a file in the same package changes, or when a package it depends on changes. This means, most of the time, that even small changes require recompiling quite a lot of code. Contrast with C, where most of the time I’m working on just a single source file, where a recompile means compiling a single file and re-linking.
C’s compilation model is pretty bad and often results in unnecessary work, especially as it’s used in C++, but it means that you can just work on an implementation by just recompiling a single file every build.
I have not encountered many packages that take more than one second to compile. And the Go compiler typically parallelizes compilation at the package level, further improving things. I’m curious to see any counter examples, if you have them.
Indeed, errors as regular values enable some elegant patterns that are cumbersome when errors (exceptions) are tied to control flow.
However, Go’s particular implementation of errors-as-values leaves a lot to be desired. Being based on multiple return rather than sum types (enums with data), it can’t capture results as a whole as a value, so it’s missing out on even more elegance from expressing fallible function results as values. And of course it’s famously verbose. Other languages have shown that with a bit of syntax sugar it’s possible have best of both worlds: code almost as noise-free as exception-based error handling, preventing accidentally-unhandled errors, while still having flexibility of errors as values.
I am going to be facetious, but there is a point to this post, I promise.
it’s missing out on even more elegance from expressing fallible function results as values. And of course it’s famously verbose.
On the contrary, Go exposes the reality of what it means to simply send an error up the stack without decorating it. Go forces the programmer to think about precise error reporting!
it’s possible have best of both worlds: code almost as noise-free as exception-based error handling,
Someone’s noise is someone else’s precision! Go sits in a wonderful middle ground between error codes and exceptions, where the frequency of if err != nil blocks encourages programmers to decorate errors more precisely than they would otherwise do when (ab)using Rust’s ? (which throws away context!), or when using Java exceptions (which tend to involve noisy stack traces!).
preventing accidentally-unhandled errors
Linters exist! It’s $CURRENT_YEAR, you should have a linter in your CI script anyway, why not give it an extra task?
while still having flexibility of errors as values.
Not nearly as elegantly and as simply as Go allows it, through Go 1.13 error (un)wrapping and inspection!
I am exaggerating, somewhat. Perhaps my post sounds like a joke, especially when you consider how similar the statements “Go forces you to think about precise error reporting!” and “Rust forces you to think about precise ownership of memory!” sound. I suppose it is somewhat of a joke, but I am trying to point something out:
I am reminded of Bryan Cantrill’s talk about platforms and values. I think that if one is to enjoy Go error handling, one must value very precise error reporting to begin with (or eventually be molded into this attitude by using the language). It seems to me that this position is not unheard of in conversations on the internet about about the language, but most people are on the side of “Go is famously verbose”, from what I can tell. Perhaps this hints at a more general state of affairs in the industry with respect to how much people value precise error reporting.
Earnestly, Rust’s ? does indeed seem to be the best of both worlds, because it’s very easy to go from ? to error decoration, but Go doesn’t have a mechanism to compact if err != nil. Not that I’d (ever?) use such a mechanism personally, but people really seem to want it, so there is that.
I am reminded of Bryan Cantrill’s talk about platforms and values. I think that if one is to enjoy Go error handling, one must value very precise error reporting to begin with (or eventually be molded into this attitude by using the language)
My only personal annoyance with Go’s syntax has been the behavior of := with respect to desugaring the tuple/product that is returned by a function that can error out, where I’ve inadvertently lost track of an error. But yes, linters have caught this for me several times. In long code blocks with lots of IO, I also find the error handling to distract from logic, but this also doesn’t occur frequently enough in my code to bother me overly.
Earnestly, Rust’s ? does indeed seem to be the best of both worlds, because it’s very easy to go from ? to error decoration
While I find this compaction very powerful, I think it’s also a bit of a footgun. I’ve frequently seen ? use to bubble up errors and ignore them. One of my favorites that plagues the Rust ecosystem is when reading from stdin. If stdin is closed early (e.g. when using head to read the top of a file), then a read will return an Err(...), and most programs will just bail because they used a ?. I appreciate the ? tool, but its ability to stuff an error out of a programmer’s view can be troubling. I like the pattern of using custom error types everywhere but the very toplevel of the program, but that level of required discipline is exactly what makes ? a footgun in my opinion.
The elegance of Go’s error handling is you treat them like any other value. This does in fact (like the OP shows) let you build nice abstractions around errors (if you want, I don’t normally myself but each to their own). The differing ways to handle errors in other languages, be it exceptions or sum-types are just that, a different approach, each have their pros/cons. For example with languages that support exception handling, it becomes quite tempting to just have a global/top-level try/catch which can lead to defective software behavior.
Sum types are not a mechanism to handle errors. Sum types are just types that model a XOR situation. Result types in Rust are just one application of sum types, where we have Ok XOR Err. But that is just a value too, just like in Go. The only thing that is special about Result types is that the language gives you syntactic sugar for returning early if the value is an Err. But it’s the syntactic sugar that is special here, not the sum types. They are far too widely useful to reduce them to just errors. There are XORs everywhere.
EDIT: I’d say that the Go approach to errors is “sum types represented as product types”, where discriminating between the different XOR situations is left as an exercise to the programmer.
They are far too widely useful to reduce them to just errors.
Nothing in the GP’s comment did this. “Sum types are a mechanism to handle errors” is a true statement. It is not necessarily the most precise statement, but it’s still true. And the GP didn’t need more precision to make their point.
I don’t think that’s what /u/prologic meant. I think they specifically meant that sum types (indeed, “XOR types” as the intuition goes) are one way to model exceptional behavior and exceptions are another. Much like exceptions can be used to change control flow, but can also be used to handle errors.
Exceptions aren’t values. Unless you simply mean types that inherit from an Exception class. They’re special. Sum types aren’t special. They are not a way to model exceptional behaviour. They’re just values.
It’s inadvisable to use exceptions for normal unexceptional control flow. They’re not made for that purpose and it has costs accordingly, in performance and code legibility.
Sum types aren’t special. They are not a way to model exceptional behaviour. They’re just values.
Sum types may be used to model exceptional behavior. Is that a better way to put it? Much like integers can be used to represent booleans, while not making integers the same thing as booleans. This distinction feels overly pedantic to me.
It’s inadvisable to use exceptions for normal unexceptional control flow. They’re not made for that purpose and it has costs accordingly, in performance and code legibility.
That is the general advice, yes, but I’ve worked with code bases in the past that used exceptions for a bit more than just exceptional behavior. There are gray areas at that, such as choosing whether to throw an exception if an attempt is made to read past end of file.
Sum types may be used to model exceptional behavior. Is that a better way to put it? Much like integers can be used to represent booleans, while not making integers the same thing as booleans. This distinction feels overly pedantic to me.
I don’t see how that’s different from the Go situation then. Just as well, you can say “values may be used to model exceptional behaviour”. But my point is that Rust is closer to Go than to Java in its treatment of errors. Exceptions were invented for error handling, sum types weren’t. The difference between Go and Rust is that values in Rust are more expressive, where, as I wrote in the original comment, the Go convention is to encode sum types as product types, because it doesn’t have support for the former and task the programmer with differentiating between variants (if err != nil). The two languages in this area are doing essentially the same thing, but one has a compiler-assisted mechanism for doing this general thing (not made specifically for errors, but, again, just a certain class of values that’s often encountered in practice). You may observe a similar phenomenon in Rob Pike’s Lisp implementation in Go, where he encodes a sum type (atom XOR list) as a product type - exactly the same thing as happens with error handling in Go, but demonstrating that it’s more general.
I use guru for this, which allows tacking on a guru meditation number/error code to an error (such as HTTP status codes), and a little wrapper to handle it:
func Wrap(h func(http.ResponseWriter, *http.Request) error) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
err := h(w, r)
var stErr interface {
Code() int
Error() string
}
code := 500
switch {
case errors.As(err, stErr):
code = stErr.Code()
case errors.Is(err, sql.ErrNoRows):
code = 404
case errors.Is(err, context.DeadlineExceeded):
code = http.StatusGatewayTimeout
}
w.WriteHeader(code)
// write output, depending on content-type.
}
}
Can someone help me understand why, in both this article and the referenced golang.org article, they chose to solve a code-duplication problem with an interface? A function seems like a more obvious solution to me. An interface would make sense to me if you wanted to vary the behavior of WithError at runtime, but that doesn’t seem to be the case.
Could you elaborate more on what are you specifically asking about? Both the articles are quite big, I’m not sure which part of them are you referring to in particular. You seem to mention WithError - are you asking why WithError returns a http.HandlerFunc? If yes, that’s most probably to make it easy to use where a HandlerFunc is needed. But what do you mean by “code-duplication problem” or “a function […] solution” then? If not, I’m even more confused what is it that you’re trying to understand… Maybe you could call out specific function names and types to help me anchor your question?
Thanks. I really meant to ask why they chose to let viewRecord return an error in order to remove the repetition, rather than just calling a function like WithError directly from inside viewRecord. It starts under heading “Simplifying repetitive error handling” in the golang.org article.
Good lord, how is it elegant to need to turn your code inside-out to accomplish the basic error handling available in pretty much every other comparable language from the last two decades? So much of Go is well-marketed Stockholm Syndrome.
I don’t think that responding with a 404 if there are no rows in the database is that any language supports out of the box. Some frameworks do, and they all have code similar to this for it.
And sadly so often error handling is often done in a poor manner in the name of abstraction, though really bad one that effectively boils down to ignore that things can go wrong, meaning that one ends up digging through many layers when they actually do go wrong.
People eventually give up and copy paste StackOverflow[1] solutions in the hopes that one of them will work, even when the fix is more accidental and doesn’t fix the root cause.
The pinnacle was once checking code that supposedly could not fail. The reason was that every statement was wrapped in a try with an empty catch.
But back to the topic. Out of the box is all good and nice until you want to do something different which in my experience happens more often than one would think. People sometimes create workarounds. In the example of non existing rows, for example doing a count before fetch, so doing two queries instead of one, just to avoid cases where a no rows error would otherwise be thrown.
Now i am certainly not against (good) abstractions or automation, but seeing people fighting against those in many instances makes me prefer systems where they can be easily be added and can easily be reasoned about, like in this example.
[1] Nothing against StackOverflow, just blindly copy pasting things, one doesn’t even bother to understand.
In what way is Go’s error handling turning my code inside out?
Pike has set PLT back at least a decade or two.
It is possible to improve the state of the art while also having a language like Go that is practical, compiles unusually fast and is designed specifically to solve what Google found problematic with their larger C++ projects.
There is nothing unusual about it. It’s only C++ and Rust that are slow to compile. Pascal, OCaml, Zig and the upcoming Jai are decent. It’s not that Go is incredible, it’s that C++ is really terrible in this regard (not a single, but a lot of different language design decisions made it this way).
For single files, I agree. But outright disallowing unused dependencies, and designing the language so that it can be parsed in a single pass, really helps for larger projects. I agree on Zig and maybe Pascal too, but in my experience, OCaml projects can be slow to compile.
I’m enjoying tinkering with Zig but I do wonder how compile times will change as people do more and more sophisticated things with comptime.
My impression from hanging out in
#zig
is that the stage 1 compiler is known to be slow and inefficient, and is intended as a stepping-stone to the stage 2 compiler, which is shaping up to be a lot faster and more efficient.Also there’s the in-place binary patching that would allow for very fast incremental debug builds, if it pans out.
Don’t forget D.
My experience with Go is that it’s actually very slow to compile. A whole project clean build might be unusually fast, but it’s not so fast that the build takes an insignificant amount of time; it’s just better than many other languages. An incremental build, however, is slower than in most other languages I use; my C++ and C build/run/modify cycle is usually significantly faster than in Go, because its incremental builds are less precise.
In Go, incremental builds are on the package level, not the source level. A package is recompiled when either a file in the same package changes, or when a package it depends on changes. This means, most of the time, that even small changes require recompiling quite a lot of code. Contrast with C, where most of the time I’m working on just a single source file, where a recompile means compiling a single file and re-linking.
C’s compilation model is pretty bad and often results in unnecessary work, especially as it’s used in C++, but it means that you can just work on an implementation by just recompiling a single file every build.
I have not encountered many packages that take more than one second to compile. And the Go compiler typically parallelizes compilation at the package level, further improving things. I’m curious to see any counter examples, if you have them.
I don’t remember anyone in POPL publishing a PLT ordering, partial or total. Could you show me according to what PLT has been set back a decade?
Srsly, I was looking for simpler, and was disappointed by the false promise.
Indeed, errors as regular values enable some elegant patterns that are cumbersome when errors (exceptions) are tied to control flow.
However, Go’s particular implementation of errors-as-values leaves a lot to be desired. Being based on multiple return rather than sum types (enums with data), it can’t capture results as a whole as a value, so it’s missing out on even more elegance from expressing fallible function results as values. And of course it’s famously verbose. Other languages have shown that with a bit of syntax sugar it’s possible have best of both worlds: code almost as noise-free as exception-based error handling, preventing accidentally-unhandled errors, while still having flexibility of errors as values.
Rust and Zig are two languages that do Go-style error handling far better than Go does. Go feels so cumbersome in comparison.
I am going to be facetious, but there is a point to this post, I promise.
On the contrary, Go exposes the reality of what it means to simply send an error up the stack without decorating it. Go forces the programmer to think about precise error reporting!
Someone’s noise is someone else’s precision! Go sits in a wonderful middle ground between error codes and exceptions, where the frequency of
if err != nil
blocks encourages programmers to decorate errors more precisely than they would otherwise do when (ab)using Rust’s?
(which throws away context!), or when using Java exceptions (which tend to involve noisy stack traces!).Linters exist! It’s $CURRENT_YEAR, you should have a linter in your CI script anyway, why not give it an extra task?
Not nearly as elegantly and as simply as Go allows it, through Go 1.13 error (un)wrapping and inspection!
I am exaggerating, somewhat. Perhaps my post sounds like a joke, especially when you consider how similar the statements “Go forces you to think about precise error reporting!” and “Rust forces you to think about precise ownership of memory!” sound. I suppose it is somewhat of a joke, but I am trying to point something out:
I am reminded of Bryan Cantrill’s talk about platforms and values. I think that if one is to enjoy Go error handling, one must value very precise error reporting to begin with (or eventually be molded into this attitude by using the language). It seems to me that this position is not unheard of in conversations on the internet about about the language, but most people are on the side of “Go is famously verbose”, from what I can tell. Perhaps this hints at a more general state of affairs in the industry with respect to how much people value precise error reporting.
Earnestly, Rust’s
?
does indeed seem to be the best of both worlds, because it’s very easy to go from?
to error decoration, but Go doesn’t have a mechanism to compactif err != nil
. Not that I’d (ever?) use such a mechanism personally, but people really seem to want it, so there is that.My only personal annoyance with Go’s syntax has been the behavior of
:=
with respect to desugaring the tuple/product that is returned by a function that can error out, where I’ve inadvertently lost track of an error. But yes, linters have caught this for me several times. In long code blocks with lots of IO, I also find the error handling to distract from logic, but this also doesn’t occur frequently enough in my code to bother me overly.While I find this compaction very powerful, I think it’s also a bit of a footgun. I’ve frequently seen
?
use to bubble up errors and ignore them. One of my favorites that plagues the Rust ecosystem is when reading from stdin. If stdin is closed early (e.g. when usinghead
to read the top of a file), then a read will return anErr(...)
, and most programs will just bail because they used a?
. I appreciate the?
tool, but its ability to stuff an error out of a programmer’s view can be troubling. I like the pattern of using custom error types everywhere but the very toplevel of the program, but that level of required discipline is exactly what makes?
a footgun in my opinion.The elegance of Go’s error handling is you treat them like any other value. This does in fact (like the OP shows) let you build nice abstractions around errors (if you want, I don’t normally myself but each to their own). The differing ways to handle errors in other languages, be it exceptions or sum-types are just that, a different approach, each have their pros/cons. For example with languages that support exception handling, it becomes quite tempting to just have a global/top-level
try/catch
which can lead to defective software behavior.Sum types are not a mechanism to handle errors. Sum types are just types that model a XOR situation.
Result
types in Rust are just one application of sum types, where we have Ok XOR Err. But that is just a value too, just like in Go. The only thing that is special aboutResult
types is that the language gives you syntactic sugar for returning early if the value is anErr
. But it’s the syntactic sugar that is special here, not the sum types. They are far too widely useful to reduce them to just errors. There are XORs everywhere.EDIT: I’d say that the Go approach to errors is “sum types represented as product types”, where discriminating between the different XOR situations is left as an exercise to the programmer.
Nothing in the GP’s comment did this. “Sum types are a mechanism to handle errors” is a true statement. It is not necessarily the most precise statement, but it’s still true. And the GP didn’t need more precision to make their point.
I don’t think that’s what /u/prologic meant. I think they specifically meant that sum types (indeed, “XOR types” as the intuition goes) are one way to model exceptional behavior and exceptions are another. Much like exceptions can be used to change control flow, but can also be used to handle errors.
Exceptions aren’t values. Unless you simply mean types that inherit from an
Exception
class. They’re special. Sum types aren’t special. They are not a way to model exceptional behaviour. They’re just values.It’s inadvisable to use exceptions for normal unexceptional control flow. They’re not made for that purpose and it has costs accordingly, in performance and code legibility.
Sum types may be used to model exceptional behavior. Is that a better way to put it? Much like integers can be used to represent booleans, while not making integers the same thing as booleans. This distinction feels overly pedantic to me.
That is the general advice, yes, but I’ve worked with code bases in the past that used exceptions for a bit more than just exceptional behavior. There are gray areas at that, such as choosing whether to throw an exception if an attempt is made to read past end of file.
I don’t see how that’s different from the Go situation then. Just as well, you can say “values may be used to model exceptional behaviour”. But my point is that Rust is closer to Go than to Java in its treatment of errors. Exceptions were invented for error handling, sum types weren’t. The difference between Go and Rust is that values in Rust are more expressive, where, as I wrote in the original comment, the Go convention is to encode sum types as product types, because it doesn’t have support for the former and task the programmer with differentiating between variants (
if err != nil
). The two languages in this area are doing essentially the same thing, but one has a compiler-assisted mechanism for doing this general thing (not made specifically for errors, but, again, just a certain class of values that’s often encountered in practice). You may observe a similar phenomenon in Rob Pike’s Lisp implementation in Go, where he encodes a sum type (atom XOR list) as a product type - exactly the same thing as happens with error handling in Go, but demonstrating that it’s more general.I use guru for this, which allows tacking on a guru meditation number/error code to an error (such as HTTP status codes), and a little wrapper to handle it:
Can someone help me understand why, in both this article and the referenced golang.org article, they chose to solve a code-duplication problem with an interface? A function seems like a more obvious solution to me. An interface would make sense to me if you wanted to vary the behavior of WithError at runtime, but that doesn’t seem to be the case.
Could you elaborate more on what are you specifically asking about? Both the articles are quite big, I’m not sure which part of them are you referring to in particular. You seem to mention WithError - are you asking why WithError returns a http.HandlerFunc? If yes, that’s most probably to make it easy to use where a HandlerFunc is needed. But what do you mean by “code-duplication problem” or “a function […] solution” then? If not, I’m even more confused what is it that you’re trying to understand… Maybe you could call out specific function names and types to help me anchor your question?
Thanks. I really meant to ask why they chose to let viewRecord return an error in order to remove the repetition, rather than just calling a function like WithError directly from inside viewRecord. It starts under heading “Simplifying repetitive error handling” in the golang.org article.
[Comment removed by author]