Hacker News new | past | comments | ask | show | jobs | submit login
Go’ing Insane: Endless Error Handling (jesseduffield.com)
147 points by genericlemon24 9 days ago | hide | past | favorite | 211 comments





Maybe it’s just Stockholm Syndrome, but IMO Go’s “verbose” error handling approach is a feature, not a bug. I think the expectation that every single error is handled and (importantly) wrapped explicitly is one of the reasons why Go and the Go ecosystem in general is conducive to writing robust and resilient systems. When writing Go, error handling is at the forefront of my mind, and not an afterthought, as it can be in other languages.

Have you used Rust? Because it has the same explicit error handling, except it has syntax sugar (the '?' operator) which makes it painless. Additionally it's robustness is even better than Go's, as the Result type it uses for errors makes it impossible (compile time error) to access the return value without first checking for an error.

For comparison:

  fn my_func() -> Result<(), Error> {
      foo()?;
      bar()?;
      baz()?;
      Ok(())
  }

  // ---

  let val = bar()?;
  println!("{}", val);

  // ---

  fn my_func() -> Result<i32, Error> {
      foo()?;
      bar()?;
      let val = baz()?;
      // ... more stuff
      Ok(val)
  }

  // ---

  fn my_func() -> Result<(i32, String), Error> {
      foo()?;
      bar()?;
      let (val1, val2) = baz()?;
      Ok((val1, val2))
      // Those last two lines could even be just `baz()` or `Ok(baz()?)`.
  }
Rather close to the world the author wants to live in.

What happens if you want to log something before returning the error, or wrap the error, add some context or something ? Is there a way to override the ? operator? Or do you fallback to a matcher, which is roughly the same as Go error handling?

If you want to manually wrap/change the error somehow then the pattern would be something like:

     foo().map_err(|err| WrapperError::new(err))?
For context, typically you would use a library like `anyhow` which allows you to do:

     foo().context("Context string here")?
or

     foo().with_context(|| format!("dyanmically generated context {}", bar))?
I don't know of any pre-built solutions for logging, but you could easily add a method to Result yourself that logged like:

    foo().log("log message")?
or something like:

    foo().log(logger, 'info', "log message")?
The code to do this would be something like:

    use log::error;
    
    trait LogError {
        fn log(self, message: impl Display) -> Self;
    }
    
    impl<T, E> LogError for Result<T, E> {
        fn log(self, message: impl Display) -> Self {
            error!("{}", message);
            self
        }
    }
Then you'd just need to import the `LogError` error trait at the top of your file to make the `.log()` method available on errors.

Nice to see so many people mentioning `map_err`. This is exactly what I've done when working with Rust (I have professional experience with both, slightly more with Go) and it's a dream compared with Go's error handling.

What if you want to log, then emit some time series data?

Is chaining+traits+wrappers+map_err truly less complex than a conditional:

  if BAD {
    Log()
    Metric1()
    Metric2()
  }

If you're doing something more complex/involved in order to handle a particular error, then no I think a conditional/match is fine. It's the common case when you're not doing any special that the boilerplate becomes overwhelming.

Having said that, if you're outputting the same metrics for every error then a method that does this for you makes a lot more sense than repeating yourself all over the place.


And this is the beauty of chaining and extension traits: you can define your own “shorthands” without much trouble, because it’s all in the normal type system.

Remember also that .map_err() need not be significantly different from that if block; it can be essentially just a slightly different spelling:

  let data = some_call().map_err(|e| {
      log();
      metric1();
      metric2();
      e
  })?;
But I would draw your attention in all this to the fact that you’re dealing with algebraic data types, enums with data attached to the variants; this isn’t just some nullable data value and nullable error where you can say `if error { … }`; in order to get at the data, you need to handle the error. This has profound implications that, once you’re used to it, will shape your coding style and techniques even when you return to other languages.

On things like metrics, I’d also mention that sometimes you may be better served by an RAII pattern, which can be made to be the rough equivalent of a defer statement in Go. Especially if you’re pairing start/end metrics, RAII can be particularly good at that.


The commenter you're replying to is simply showing how logging and context and other things can be done in conjunction with the question mark syntax sugar. If you don't want to use the question mark at all, you're still free to explicitly pattern match on the Result object and do whatever complex thing you want with the error object before returning it.

In the code shown, `foo()?;` is equivalent to this:

  match foo() {
      Ok(val) => val,
      Err(err) => return Err(err.into()),
  };
(This isn’t exactly how it’s implemented—that detail is unstable and has actually changed <https://github.com/rust-lang/rfcs/pull/3058> since ? was stabilised, without disrupting things!—but it’s equivalent in this instance. As examples of how it’s not exact: you can use ? on Option too; and inside a try block (unstable), ? isn’t a return, acting more like a break-with-value.)

For wrapping the error: note the .into() in the desugaring: it’ll perform any necessary type conversions which is the normal way you’d do error wrapping. But if you want to do anything more than that, or adding specific context, you’re still in luck! You can use method chaining to manipulate the result. In the standard library is map_err <https://doc.rust-lang.org/std/result/enum.Result.html#method...>:

  foo().map_err(|err| f(err))?
And you can add other methods to such types with extension traits; as an example, the anyhow crate, popular for application-level error handling (where you don’t care about precise modelling of errors) makes it so that, on importing anyhow::Context <https://docs.rs/anyhow/1.0.44/anyhow/trait.Context.html>, you can add context to an error like this:

  foo().context("foo failed")?
  foo().with_context(|| "foo failed")?

Yeah, if you need to explicitly do something with the error then you fall back to a matcher. But the idea is that you only need to deal with errors at error boundaries which you can define. So if you just want to pass an error up the stack to be handled somewhere else then you can with almost no syntactic overhead.

With something like a functional effect system you can even do better than that. For instance with Scala ZIO you can do something like:

``` val result = for { fooResult <- foo().tapError(e => console.putStrLn(s"Error in foo!: $e") barResult <- bar(fooResult).tapError(e => console.putStrLn(s"Error in bar!: $e") bazResult <- baz(barResult) } yield bazResult ```

Then when you run the bazResult effect you get either an error or the result type which you can handle however you like.


The `Result` type has a `map_err` method to which you can pass a function (or closure) to modify the error, so you can write something like...

``` let (val1, val2) = baz() .map_err(|err| add_my_context(err, "some context"))?; ```

Also, the `?` operator automatically calls `into()` on the error that it was given, which will convert it into the desired error type (assuming there's a conversion implemented for it), so you only need the `map_err()` approach when you need to add context.

I'm not sure what the idiomatic Rust way of logging before returning the error is - in the code I've written (and read), you'd add return the error with added context and leave the handler to decide whether to log.

In my experience, it is pretty ergonomic.


The operator is just syntax sugar for something roughly this

    let foo = foo()?;

    // Expanded
    let foo = match foo() {
      Ok(v) => v,
      Err(e) => return Err(From::from(e))
    };
If you're using your own error type then you can impl From/Into yourself to capture additional context from the source error (or a stacktrace) in addition to using map_err() as others have suggested

You can .map_err() before the ? to transform it inline using a closure

I don't know Rust myself but, where / how are the errors actually handled? Or is this like unchecked exceptions?

Rust’s Result-based error handling is fairly similar to checked exceptions—all errors must be handled, whether you do something with them, ignore them or propagate them to the caller—but without the pain of checked exceptions, because the errors are just a normal part of the type system, which makes them much easier to work with in various ways (some of which are seen in the various comments here): the return type Result<T, E> is either a T (labelled “Ok”), or an E (labelled “Err”).

The question mark operator just gives you an easy way of doing perhaps the most common handling of errors, which is propagating it up to the caller to worry about. So somewhere along the way you’re going to need to actually handle it (or explicitly ignore it), though it’s also possible in simple programs to let Rust handle it, by having your main function return a Result (that is, something like `fn main() -> Result<(), Error>`), in which case if it’s an Err it prints the error (roughly `eprintln!("Error: {:?}", err);`) and exits with status code 1.


Like Go, the error is just returned from the function. The Result<> type is literally defined as “either an error value or a success value”.

It’s up to the caller what to do with an error - panic, return it in turn, wrap it, or whatever else makes sense in context. It’s semantically quite similar to Go - except with better library support (Result instead of returning a tuple) and better syntax (the “?” operator instead of manual checks).


Exceptions are a problem because they're a control flow change, whereas perhaps the error condition is just data.

For truly exceptional situations, Exceptions are a good match because actually you wanted a control flow change. If the NetworkFailed maybe all of this complicated multi-node-synchronisation code is useless and we should handle that in code for NetworkFailedExceptions.

But one program's exceptional situation is another's business as usual, our desktop network visualisation icon does not want to eat a NetworkFailedException, the fact that NetworkFailed is just a reason to change the little icon to red instead of green, not exceptional at all.

This is particularly egregious when doing large data processing. If I doBackups() sending forty different sources to six backup servers, I actually don't want the failure of one backup server to handle one of the forty sources to blow up the entire function, I would much rather get back a detailed overview and go OK, 5 out of 6 ain't bad, no need to set off alarm bells and wake up the sysadmins. Error handling Go's way makes this a bit ugly but practical, Rust's way makes it feel reasonably ergonomic, in languages like C++ or Java it was kinda horrible (but C++ is slated to get an Expected class to mostly fix this in C++ 23).


I haven’t used Rust. Does the ? operator add some kind of stack trace to the error? If not, how do you know where the error has come from?

Having a Result type with statically enforced checking sounds good though.


> I haven’t used Rust. Does the ? operator add some kind of stack trace to the error?

No. `?` performs an optional conversion and an early return, that’s it (kinda).

> If not, how do you know where the error has come from?

By default you don’t. There are utility libraries you can use to attach such information to error types (and eventually the stdlib will provide one), but OOTB an error value is… quite literally anything (it should probably implement `Error` but doesn’t actually have to).

This is in keeping with the idea that hidden costs should be avoided when possible, and that reified results should be the norm so should have as little requirement as possible.


> Does the ? operator add some kind of stack trace

It depends. `?` will convert the inner error type (the value from the function to its left) to the outer error type (the return type of the function containing this line). That conversion function can be defined by the inner error type. If the conversion generates a stack trace (or adds to an existing one), then there will be one.

Therefore, "Is there always a stack trace available?" is a tricky question-- it was seen as unimportant during the 1.0 days because they wanted to be able to accommodate low-resource environments.

Today, there's third party error handling libraries that cut out error creation / manipulation boilerplate, that might create stack traces for you, and there's efforts ongoing to see how it might be added to the stdlib.


Doing foo()? is equivalent to:

  err := foo()
  if err != nil {
    return err
  }

exactly! a little sugar would be most welcome.

[flagged]


No, the response in this case was entirely pertinent:

jamespwilliams: It is good for (Go's) error handling to be verbose, because it ensures that you handle every error instead of forgetting or ignoring them.

nicoburns: It is possible to ensure every error is handled instead of forgotten or ignored without the need for verbosity, as demonstrated by Rust.


> No, the response in this case was entirely pertinent

Yeah, they always say that. How convenient that it constantly involves bringing up Rust Rust Rust when the thread is nominally about some other language. Just look at other HN top threads, it's a common pattern when PL's are discussed. And it's something that people will mistakenly blame on the Rust community.


Have you considered that many PL hobbyists and/or enthusiasts recognize that many features of Rust (or other languages that may be brought up out of context, though in this case Rust is appropriate given the article content) were carefully, deliberately designed? It comes up often, I think, because Rust is one of only a handful of languages I can think of that is incorporating lessons learned, best practice, and innovations into every facet of its design and surrounding tooling. People who have researched, used, and (sometimes) suffered from a great variety of programming languages recognize the good that's being replicated and the bad that's being avoided.

Aside from all of this, even if the article hadn't itself mentioned Rust, an article about a programming language's feature(s) is completely fair game to compare to other languages.


> were carefully, deliberately designed?

Look, this constant insistence by Rust evangelists that Rust is the only language that was "carefully, deliberately designed" is part of the problem. It makes Rust proponents look foolish and arrogant. There are ways of doing language advocacy or discussing language design while still being kind, empathetic and considerate of other perspectives and points of view. I should note that I have never seen this issue happening from within the Rust community, it's always come from outsiders' careless "advocacy".


Rust is not an innovator. But "Rust..." is going to show up where people could have said a dozen obscure languages nobody heard of that did innovate these features, precisely because nobody heard of them. The other examples are often less relevant. For example when it comes to the use of Sum type error handling, you could say "Like Expect in C++ 23" only that's still a proposal, so you can't say much definitive about it, whereas Result in Rust is here and in wide use today. Or they could say it's like the Swift sum types, but in this context it's confusing because although Swift uses sum types for error handling it has a very different affordance through exception like behaviour.

When we say e.g. some English dialects have Negative Concord ("Nobody ain't see nothing"), we aren't implying that this is because English invented Negative Concord, we're mentioning this because the English-using audience here have seen and likely even used Negative Concord in English whereas chances are most people here aren't familiar with lots of other languages where Negative Concord is widespread and so examples from those languages are less helpful.

Also, lots of people seem to really like Rust. I like Rust.


People use Rust as an example when talking about Go because Rust could live in the same space as Go, and more people know about Rust than Haskell/OCaml/SML. I think OCaml should be what Go could be compared to, as when Go was created it was already solid[+]. Rust came after and took inspiration from OCaml and others. People criticize Go because Rust learned some important lessons from other languages, and Go didn't. Some people will argue that it's a matter of taste, but some Go users would love something closer to ML (https://go.dev/blog/survey2020/missing_features.svg, https://go.dev/blog/survey2020-results).

[+]: Of course there's the multicore story. OCaml is soon getting it (in a backwards-compatible way even!) but at the time Go was created it wasn't ready. Still, from what I remember there were some multicore ML that existed, and considering most of the work that went into Go was around the standard library and tooling, they could easily have made popular a different language. Some people will argue about "simplicity" and "Go being close to C", but I think that by "simplicity" most people mean "actually good tooling, GC'd, compile to a single binary fast".


I didn't say Rust is the only language. The preceding parenthetical immediately includes other modern languages as well. I was asking you to see a different perspective. I can't say I've seen the evangelism you've seen; most comments are as appropriately placed and relevant as one can expect on a global forum (including the original comment here). I'm not saying evangelism doesn't exist, but our experiences differ.

the article itself compares Go's error handling to Rust's, so mentioning Rust in the comments is quite appropriate. Even if the article did not mention Rust, it makes no sense to evaluate a language's feature in a vacuum: comparisons with other languages ground the analysis in reality

The ? operator permits chaining, which obscures sad path control flow and makes code harder to reason about, not easier.

> Maybe it’s just Stockholm Syndrome, but IMO Go’s “verbose” error handling approach is a feature, not a bug.

No offense, but I think it's Stockholm Syndrome. It reminds me of what the JavaScript folks used to say 5 years ago: "No, it's not a terrible language. I love JavaScript's callback hell." Of course once it gained promises/await, now they _really_ love it.

I've been using Go for the last year and while I do genuinely enjoy the simplicity of the language, Go does have these annoying little customs like pebbles in your shoe.

You can _say_ it forces you to think about error handling, but I find it does not. To be honest, I'm able to not think about error handling just fine. It just forces me to write boilerplate when I'm trying to crank out a quick non-robust prototype.


I prefer Kotlin on this front. They are interoperable with Java but they downgraded Java's checked exceptions to non checked exceptions. So, they don't force you to litter your code with try .. catch or throws statements. Additionally, with recent releases they use value classes for representing outcomes where failure is an expected outcome (i.e. not exceptional). That's similar to what Scala and other functional languages are doing. That and de-structuring makes dealing with errors a lot less tedious. It's actually not that dissimilar from what go does but just a bit nicer to deal with.

The whole attitude with Kotlin is that exceptions are exceptional and that you probably won't be doing anything productive with them beyond logging. Using exceptions for conditional logic is a bit of a design smell. Sometimes you have to with some older Java libraries. But it's not what you are supposed to do. Otherwise, catch exceptions centrally. When using co-routines, uncaught exceptions actually cancel the co-routine scope (and the the other co-routines in them, if any), which gives you an opportunity to deal with that in a sane way. You can have error handlers attached to those to deal with that. Structured concurrency is a buzzword they like to use for this notion.

I've been on a few Go projects; never as a main developer but enough to understand what is happening. I appreciate its simplicity; it's easy to get into. That is IMHO the main value of go: getting stuff done. But the verbosity is a bit painful if you come from something higher level. With Kotlin's native compiler slowly getting usable, I could see myself opting for that where I might have used Go or something else in the past (e.g. command line tools).


The fact that error handling is "at the forefront of your mind" is exactly the problem. Instead of following the code, you have to constantly deal with the irrelevant detail of error wrapping. 99.9% of code doesn't handle errors - it returns them further up the call stack (in rare cases with some extra context). This is exactly what exceptions do, without someone having to constantly write code for them to do so.

When I'm writing Java, I have to keep mental track of the fact that nearly anything might invisibly "return" from in the middle of a function. Checked exceptions help keep a handle on that, but in general they still usually need to be caught and rewritten into something that doesn't leak encapsulation-breaking implementation details. In that that regard, it's the same boilerplate as Go - if, usually, unhelpfully far away from the code that might throw the exception. There is a reason writing Java really needs an IDE. Just looking at the text doesn't tell you "what might throw this?"

When I'm writing Go, it returns where it says "return".


When reading Java code you only need to pay attention to whether any resource that is acquired is also safely released - i.e. that no resources are acquired without using `using` or `try/finally`. This is the same as Go with `defer`.

Beyond that, you don't need to care if the function will terminate early because of an exception - as long as all necessary cleanup is handled regardless of exit reason, you don't care that the function will finish early if an HTTP request fails before using the value of the request.

And usually you only want to catch and re-throw exceptions around module (in the broad sense) boundaries. If you catch and re-throw exceptions in every function, you're either losing lower-level context completely OR you're leaking implementation details anyway if you're chaining up the old error.

In Go, the error needs to be manually propagated through every function, with enough context to understand what's going on. In Java, the error needs to be manually handled at every module boundary; function-level context is given automatically by the call-stack.


Java doesn't require you to handle exceptions. This is not the criteria you should use to determine if you need handle the exception.

The fact is that even code that does no resource acquiring could throw an exception. It might not even be a documented exception. And when it throws that exception that you may or may not have been expecting some code somewhere is going to have to figure out what to do. In java the only real way to communicate an error is to throw an exception. For some reason culturally Java has decided that it's too much work to distinguish between errors that should crash the app and errors that should be handled by the caller in some way. As a result when an exception outside of your code happens in production and you read the exception in your logs it will:

1. Not have a significant part of the information you need to debug the problem.

2. Be in code you may not have easy access to go read.

3. You will probably have no idea why it's happening.

Eventually for a long enough lived codebase you will have encountered and documented all of these in a runbook somewhere or you won't and everyone will have to have learned them over and over again.

If you are on an exceptional team you'll have caught and then wrapped or handled these exceptions to make operating the software in production less painful. But all of it could have been avoided if java had chosen a better path.

Make it impossible trap runtime exceptions. They just crash the program. Make trappable exceptions checked exceptions. The code would get more verbose. But the operational pain would have been vastly reduced.

Errors are a part of your API. Unchecked Runtime exceptions pretend like 50% of your API surface doesn't exist.


Normally, all server-style apps have some top-level error handlers that log the error and abandon or perhaps restart some piece of work (such as an HTTP request).

If these errors are things that the system administrator can handle, they will find them in the logs, the cause of the error will hopefully be clear enough if enough effort was spent on making it that way, and the sysadmin will fix the cause. If the error is something that the programmers never anticipated, nothing more can be done and a bug needs to be open with the developer. In some cases, even though the problem was with the environment, error reporting may have been fudged and the sysadmin may not be able to understand what they are suppose to fix - bad error reporting.

I fail to see how exceptions hurt with any of these cases. I have no more or less information about error cases seeing `func foo() error` or `void foo() throws Exception`, so neither helps me know what errors I should handle. It's no harder in Java to add context to errors when you really want/can - in fact, it's easier:

  try{ 
    stuff1(); 
    stuff2(); 
  } catch (Exception e) { 
    throw SpecificError("I was doing this when something else happened", e);
  }
vs

  err = stuff1(); 
  if err != nil { 
    return fmt.Errorf("I was doing this with stuff1 when something else happened: %w", err)
  } 
  err = stuff2(); 
  if err != nil {
    return fmt.Errorf("I was doing this with stuff2 when something else happened: %w", err)
    //note: different error message, since we're missing call stacks to know where this actually failed
  }
Errors are part of your API - agreed. In C# or Java without checked exceptions, you have to assume that any function can throw any exception. In Go, you get exactly 1 more bit of information: a function tells you IF it returns an error or not - same as modern C++ with `noexcept`. But if you actually want to know what errors are returned, you're SOL in most languages (Java with checked exceptions helps, but causes other problems). And none of these languages helps you with adding context to errors, except the so so context of stack traces in C# and Java.

We are so close to being in violent agreement.

I find that extra bit of information to be crucial you do not. I think you are perhaps discounting the value that Go allows you to add in your apis by making the error type explicit which then does tell you what error you might be getting. Go forces the programmer to make the fact that there is an error explicit which is good. If it also forced the developer to be explicit in which type of error it can return that would also be good since it would enforce API boundaries for errors. But I think Go is still ahead of the game compared to Java on this one.


Yes, I can see how valuing the error/no error distinction differently can significantly color the whole discussion. So, cheers for finding the point where we actually differ!

The fact it’s explicit is a good thing though. Exceptions are a nightmare to handle and a worse mental overhead when dealing with code because you have to keep the possible exceptions in your mind.

You don't need to keep possible exceptions in your mind to any more or less extent in Java than in Go.

Resources that need to be closed at function exit must be wrapped with `using` or `try/finally` in Java; with `defer` in Go.

Functions that can handle specific errors from a sub-function need to know about that error type in Java, error type or error value in Go.

Functions that can't handle any specific errors from their callees don't need to do anything in Java; they need to propagate any error in Go.

It's true that in Java it's less clear where a function can end, since any statement can potentially throw. This is technically true in Go as well with panic(), but let's accept that that is much more rarely used. But either way, if the function has any cleanup to do, that cleanup must be done in try/finally or defer, otherwise the function is brittle. So, why do I care if the function can throw an exception in the middle?


You can still be explicit without being verbose, as we've seen with rust's `?` operator

There have been attempts at making a Go syntax to do that. So far, not successfully [1].

1. https://www.infoq.com/news/2019/07/go-try-proposal-rejected/


Or use an IDE that parses the language and helps you not notice that important parts of your program's behaviour are not visible from its text.

But that's just it: when writing code the right way, there are no important parts of the code's behavior that are not visible from it's text.

For example, this code:

  void foo() {
    var res = acquireResource();
    useResource(res);
    releaseResource(res);
  }
is wrong even if no errors can be thrown. By contrast, this code is correct, regardless of whether any of these functions can throw:

  void foo() {
    try {
      var res = acquireResource();
      useResource(res);
    } finally {
      releaseResource(res);
    }
   }
Similarly, in Go this function is wrong:

  func foo() error {
    res, err := acquireResource()
    if err != nil {
      return fmt.Errorf("error acquiring resources while foo-ing: %w", err)
    }
    err = useResource(res)
    if err != nil {
      err = fmt.Errorf("error using resource while foo-ing: %w", err)
    }
    releaseErr := releaseResource(res)
    if releaseErr != nil {
      err = fmt.Errorf("%w; failed to releasing resource while foo-ing: %w", err, errW)
    }
    return err
  }
even though I don't have any exceptions.

The correct way to do this in Go would be:

  func foo() error {
    res, err := acquireResource()
    if err != nil {
      return fmt.Errorf("error acquiring resources while foo-ing: %w", err)
    }
    defer func() {
      err := releaseResource(res)
      if releaseErr != nil {
        logInSomeWay(fmt.Errorf("failed to releasing resource while foo-ing: %w", err))
      }
    }()
    err = useResource(res)
    if err != nil {
      return fmt.Errorf("error using resource while foo-ing: %w", err)
    }
  }
So overall, whether a function finishes early SHOULD be irrelevant.

That... is a very long winded way to miss the point.

If you want to see what throws "FooException", you need an IDE to do it, unless you have the whole Javadoc memorised. It might be a hundred lines up and in the middle of a foo().bar().baz() chained series of method calls.

And yes, you need to know. The same exception in two different places might mean two very different things in terms of what actually broke, or what you should do to recover or fail the task at hand. Because of this, bubbling up raw exceptions out of deeply nested functions is generally a code smell, because by the time they get to the top level of even a module, all you know is "it broke". Unless you did the donkey work of wrapping the raw exception in something with more semantic context. The same work you'd do in Go.


> If you want to see what throws "FooException", you need an IDE to do it, unless you have the whole Javadoc memorised. It might be a hundred lines up and in the middle of a foo().bar().baz() chained series of method calls.

This isn't any different in Go. Tracing Exceptions is actually often easier since they have unique names that you can grep for, whereas err is always err, so your only recourse is to trace across the callstack.

It's, like, actually worse.

> Because of this, bubbling up raw exceptions out of deeply nested functions is generally a code smell, because by the time they get to the top level of even a module, all you know is "it broke". Unless you did the donkey work of wrapping the raw exception in something with more semantic context.

What? If you have a decent root level error message, that + a stack trace is almost always enough, and is as good or better than what you get in go, since the errors are more structured. I'd generally consider the donkey work you're describing an antipattern. Adding extra context or re-raising an exception should be done only rarely, when you have confidence that the new context you're providing is more useful than the prior (and lots of good languages support exception chaining so you get all of the context from many errors, instead of just one string).


> If you want to see what throws "FooException"

That's a completely different problem, and one that is not unique to errors or exceptions. For any polymorphic type where you want to handle different variants differently, you need to know the possible variants, and this information is, by definition, not present in the code sending the polymorphic value. This is true whether you want to handle exceptions in C++, Optional t in Haskell, or interface values in Go.

> Because of this, bubbling up raw exceptions out of deeply nested functions is generally a code smell, because by the time they get to the top level of even a module, all you know is "it broke". Unless you did the donkey work of wrapping the raw exception in something with more semantic context.

Wrapping up exceptions at every function is a huge code smell. You sometimes want some level of wrapping, especially around module boundaries or in functions with highly relevant context along the way, but generally most functions along a call stack don't have anything to add to a lower level exception - especially if the lower-level library is well made. For example, fs.PathError in Go and System.IO.FileNotFoundException in C# include a way to programatically find the name of the file that was not found, so even if thrown from a low level, a higher level still has the most relevant context available without wrapping. In contrast, java.io.FileNotFoundException does not include this information unless you parse the error string, so you would have a reason to wrap it with more relevant context.


Error handling is equally important to business logic. This is a core tenet of Go, an explicit design decision. If you don't agree with that statement, then you're gonna have a bad time.

I just picked a random-ish file from the standard library to see what the error handling really looks like in practice (I went for "io" because I figured that's where errors will always crop up): https://cs.opensource.google/go/go/+/master:src/io/fs/glob.g...

Does that seem reasonably representative? To a non-Go programmer, it doesn't seem that bad, but it sure does have a lot of error-handling boilerplate -- squinting at it, something around 50% of the non-comment lines? It just seems kind of weird, if you have to write "err != nil" and "return nil, err" over and over, not to have any syntactic sugar for it!

There's what looks like missing return values on lines 64 and 69, then down on line 93 there's an explicit "return // ignore I/O error".

Is this file outdated, or otherwise not a good example? I used Google to find it, and it says "Copyright 2020" in the header.


it's not outdated, and it's a fine example - reading the go stdlib is the best way to learn the language well, honestly.

The missing return values on 64 and 69 are the "Named Return Values" thing that OP mentions - the returns are the variables named in the function declaration.


Aha, that explains it, thanks!

The older I get, the cleaner I want my code to be. Reading code is the best way to prevent bugs. If the code is strewn with error handling ceremony then it's hard to get to the essence of what the code is doing, and it makes it more likely that bugs will go unnoticed.

Personally I think Go code is incredibly clean, even with the error handling. It's just little if err != Nil statements that you essentially speed-read. And is knowing what errors are possible in your code not an important part of reading it? Why should that be obscured?

That’s why checked exceptions are good!

That's funny when Go's main focus is readability at the cost of writability. That's one of its main tenets...

Some HN comments toward Go are a bit baffling.


I find golang much harder to read due to its extreme verbosity that detracts away from the logic the code is trying to do.

Agreed. I wasn't originally a fan of Go's error handling, but I do find it forces me to explicitly handle the error, I very rarely use a plain `return err` statement, instead wrapping it with additional parameters.

Whereas when I use Rust's Result types, I tend to not add any additional context with `map_err` (until I need to investigate the cause of an error and have to retrofit it).

There's still definite room for improvement though (I think the Go 2 proposal will help a lot).


> I think the expectation that every single error is handled and (importantly) wrapped explicitly is one of the reasons why Go and the Go ecosystem in general is conducive to writing robust and resilient systems.

Sum types and pattern matching to enforce that would be even bettern. What I don't like about Go error handling isn't that it's "verbose" but that it's primitive.


I agree with this point. Errors is a natural important part of programming. You can try to make error handling easier through sugar, but in the end you need to handle the error one way or the other. A proper sum-type would help in Go, but that's at best a more convenient and better way of handling the error. The error doesn't magically disappear.

I find people who complain about Go's error handling don't understand how to program with Data. They want to type everything out. I say make a list of data, then loop through it once. Ruby people often seem to think, I know, I'll just write the code each date item at a time. Especially in unit tests. Wow.

So if you think Data first, Go is great. If you think in terms of method chaining and verbs or nouns first, then Go won't treat you kindly.


I happen to agree with the author here, but I think Go has cleverly inoculated itself against this kind of criticism by driving away the sort of people who care about this kind of thing.

People who use Go generally don't care about the tedious error handling or lack of generics or whatnot; people who do care about those things generally don't use Go.


I've thought about this too: different people have different values and I think I've found myself in a situation where Go simply doesn't align with my values. I'm not claiming that Go's values are therefore wrong, just that they're at odds with mine.

Yes! I'm trying not to sound smug or divisive. Plenty of good software is written in Go so it obviously has something going for it.

I'd love to hear from somebody who reluctantly tried Go despite initially caring about this stuff, and was won over.


I guess I would fit that bill, so I'll give it a shot.

I still care about stuff like this, and there are definitely ergonomic improvements that could be made.

I guess my feeling is that overall, for the programs I write (network services, broadly speaking), the benefits greatly outweigh the annoyances.

on the positive site, in my experience, Go:

    - Is easy to learn
    - Has a relatively small surface area
    - Generates static binaries
    - Forces you to consider error handling
      - (I'm listing this as a benefit as well as it's a downside!)
    - Is pretty darn fast, both at build time and run time
    - Is mostly well documented (text/template documentation excluded)
    - Takes backwards compatibility seriously
    - Cross compiles easily
    - Has generally very good and fast tooling
    - Has superb network-related libraries
    - Has excellent support for concurrency

on the negative side, it:

    - Is often not pretty or elegant
      - It does have an aesthetic of its own once you're used to it, but it's never as satisfying as nailing something exactly right in haskell or rust, or doing something highly dynamic and fancy in python or ruby
    - Sometimes resists nice algorithms and data structures
      - this is why I would never want to use it for data science, for example
    - Is very opinionated 
      - if you want something it doesn't agree with, you just don't get that thing

For much of the work I do, the positives outweigh the negatives. Maybe that helps?

> - Forces you to consider error handling

If you use the result. Its easy to overlook that some more procedural functions might return only an optional err value.

See also stuff like append sometimes mutates and returns a reference to a slice with the same backing array, and sometimes returns a slice with a different backing array. Nothing going to remind you you forgot to copy before returning.


for sure, I've been bitten by that as well. I really wish it were required to handle the err value.

Sounds like you're that rare beast, a persuadable swing voter :)

I love "smart" programming languages and gladly write in Rust when it makes sense. However Go wins the bulk of my language picks these days, marginally thanks to the efficient tooling and complete standard library but essentially because of how easy it is to read / review / integrate / patch external code without the need to rediscover authors' specific styles and ways of doing things.

I've always thought that Ruby and Go are on the opposite ends of the spectrum when it comes to design. Ruby barely has a spec; Go specs first. You learn Ruby by experiencing it; you learn Go by reading the spec (or similar) and package documentation. Ruby is interpreted; Go is compiled.

Yes, there are quirks in the Go language but you have to make design decisions.

Yes. People who write in Go value these things. I don't value what Ruby values. And that's okay. My Go programs will require fewer tests and run faster and still compile years later.


The Go language has a spec, but the "batteries included" standard library is full of wild, unexpected behavior: https://fasterthanli.me/articles/i-want-off-mr-golangs-wild-... It's really not a more reliable platform than Ruby.

not even go developers find the error handling adequate. Since if it was then slice acces would return an error rather than panic.

a,err:=slice[i] ...


Go isn’t alone there. Swift arrays also raise fatalError on out-of-bounds access by default, which seems incredible in an otherwise “safe” language. It’s easy to write your own accessor that returns an optional, but why isn’t that the default??

Do you also believe that the / operator should return two values, a “primary” result and an error value?

if golang error handling is a good way of handling errors that should be the correct implementation

Your thinking is too black and white.

lol this is a great point. Go's attitude is tribal towards these things -- "if you can't see why this is a good thing, the language is not for you". I say this as a huge Go fan and daily practitioner.

I appreciate this attitude. Minimalism feels a core value in Go. Bowing to the whims of every blogger would ruin that.

More like "Endless Error Handling Blog Posts". For all the big talk about how horrible the error handling is, it's really not a big deal in my day-to-day (having written Go code pretty regularly for over six years now).

For instance, if you have a function that does multiple things in sequence, how common is it to reorder the individual steps? Most times you have an intrinsic ordering between each step. And even if you reorder at some point, flipping some ":=" into "=" or vice versa based on compiler errors is not as big of a deal as the author makes it out to be.


I don't agree to the := and = issue, I think it's really bothersome primarily for the reasons that the author highlights. Unpacking is indeed inconsistent with its conditional bypasses on the := double-assignment checks. The committee's resistance against syntactic sugar like ? on this is also bothersome.

I hadn't looked at go for a few years but := is terrible for several other reasons too: - small difference in visible syntax (easy to miss) - less clear than explicit var - most crucially, can't be used for const declarations but in most cases where it's used (including the blog post) const should have been used (return from a called function that won't be modified by the caller).

Ironically, "easy to miss" is my objection about Rust's "?" suffix

It's a fair point (and the same objection could be made to Swift's ! and ?). Over time I have given up my objections to verboseness... Clarity of code is far more important than conciseness.

I do see the point for some syntactic sugar for error handling though, long if chains are terrible for clarity as well. And arguably ? and ! are typographic symbols our brains are well trained to spot.


When I was new to go I would have written one of these blog posts if I blogged. Over time I’ve come to appreciate go’s approach to errors. Nowadays I find it nerve wracking to write Ruby (my old daily driver).

I would still like to find some way to make go’s error handling a little easier on the eyes, but I’ll take this over having to be constantly vigilant about handling every possible exception.


Yeah it's really not an issue irl. It feels a bit weird at first but that's just how it is and you learn to design with it, it's not a deal breaker by any stretch of the imagination. People have been talking about it forever because it's "how to shit on golang 101"

> you learn to design with it

There is nothing to learn - it's an ugly wart you have to accept if you're using the language. There's no going around the fact that it's verbose, error-prone, and makes code hard to read and review.


> error-prone, and makes code hard to read and review.

Strong disagree on that. The fact that it’s explicit makes it extremely easy to reason about and Go code reviews are a breathe. It’s verbose, I’ll give you that, but the rest doesn’t hold.


It's true that explicit code is good, but "Go code reviews are a breeze" was not remotely my experience. For me and my past teams, reading through piles of boilerplate verbiage like this - among other 'explicit' syntax - was more of an impediment to understanding the flow of the program. Personally I think what the above commenter said is pretty much spot-on, and typical of Go teams I worked in.

> my experience.

> For me and my past teams

> piles of boilerplate verbiage like this

I'd love to have a look at your code because this isn't my experience at all. What's your background ? What's your team background ? Are you writing go code with the mindset of a java/cpp/whatever dev ?

Have a look at the source code of the top 10 go github repos, you won't find any of what the blog post talks about, because it simply isn't the way it's done


> Have a look at the source code of the top 10 go github repos, you won't find any of what the blog post talks about, because it simply isn't the way it's done

A quick look around k8s certainly shows evidence of lots of boilerplate-y error handling.

To give my perspective, I come from a primarily python background, but have used C++ and JS enough to know my way around, and Java was what I primarily learned programming in.

I write relatively little go, but I review a fair amount of it. The people whose code I'm reviewing are competent, idiomatic go programmers who follow go's style guide (https://github.com/golang/go/wiki/CodeReviewComments) to the extent that it exists.

I find it far more difficult to tease out what a stanza of go is doing compared to an equivalent stanza of python due to the visual noise. In python (and other languages), I usually use a somewhat functional/declarative style, so I use abstractions like comprehensions and such to allow terse transformations of data.

Go doesn't have these. In essence, the tradeoff go makes is to not provide abstractions in the language. This forces users to recreate them[1] ad-hoc, or suffer without abstractions at all (which doesn't scale). This makes everyone suffer, instead of just people unfamiliar with the abstractions suffer.

[1]: https://medium.com/@robertgrosse/parallelizing-enjarify-in-g..., and yeah yeah yeah go finally is getting generics, but the same general comment applies to other common and useful abstractions. Error handling, functional stuff, take your pick.


But the flow of the program isn't only or primarily about the happy path. I find it annoying when languages that don't force explicit error handling choices, force me to go searching for the non-happy path. Where do they handle the errors at? It's nowhere in this pull request. Are there errors to handle? You have to assume there must be because exceptions. Then I have to write a bunch of comments asking if they know whether this line can throw or not. Usually the answer is "I don't know" and then I'm the annoying one in the PR review because I tell them to please find out and either add a comment or handle/wrap the exception appropriately.

If the only thing you care about is the happy path in your review then Go code will annoy you. But you are also only doing half of your job in that review.


In exception-safe code, you don't care if a particular statement can throw - if the code is correct today when some line can't throw, it should still be correct tomorrow when that line can throw.

Also, it's much easier to look for catch{} blocks than it is to look for Go code that actually handles errors, since code that forwards errors, code that wraps errors, and code that handles errors all looks more or less the same in Go (if err != nil {...}).


Exactly my experience and others on the team I'm in. golang is extremely verbose as to detract from the underlying logic.

A common pattern in a function is to prepare/obtain several resources that seem disparate, and then use them together in some way. With common Go code patterns, you get lots of error "handling" (exception propagation done manually, most of the time) for the resource acquisition that you need to read through before you actually get to understanding what those resources are used for.

And until you understand what the resources are needed for, you can't actually understand if the error handling is correct.

Not to mention, to get around this, the first skill everyone reading Go code internalizes is ignoring any block that starts with `if err != nil {` that isn't more than 1-2 lines long.

In contrast, when reading Java/Python/C#/CL, exception handlers are very rare, so you actually pay attention when you see one.


...as opposed to error handling with exceptions, which is not error-prone at all and makes code really easy to review (or rather, it make it easy to get a false sense of security, and months later you find out that there is an exception you forgot to handle).

Personally I think this is a massive footgun, at least somewhat affected by ordering:

https://play.golang.org/p/oHFobaOexwR

Of course, I know now that I was 'holding it wrong'.


They should have just named it errlang.

lol

So the author's problem really comes down to "I can't re-order things when I declare them with :=". So don't. Do it like this if you think the function order is going to change:

    func foo() error {
        var err error
        err = bar()
        if err != nil {
            return err
        }
        err = baz()
        if err != nil {
            return err
        }
        return nil
     }
And the final "?" proposal doesn't actually deal with the errors. Part of the point of Go's error handling is to force us to deal with the errors. Endless "if err != nil {return err}" statements is kinda an antipattern in Go. Yes it's common, but I'm sure that's because we've been trained by exception handling to just pass the errors up.

A better (but still not perfect) pattern is to wrap the errors:

    if err != nil {
        return fmt.Errorf("tried to foo the bar with parameter %s, failed with: %w", baz, err)
    }
And obviously, an even better pattern is to actually deal with the error - retry after a delay? return a custom "abandon goroutine" error? Endlessly passing the errors up the stack until some function passes an incomprehensible error message to the user is what we're trying to get away from here.

The problem is that you don't need to mutate every error at every frame in the stack. I've seen codebases which religiously follow this rule, and most of the `fmt.Errorf` calls don't have much more to say than "error in f(): %s".

Languages that implement error handling well do not communicate error states in the form of massive strings like `call to f() failed: g() returned an error with parameter '123': error in h(), with argument x = 'interface{}{}': connection failed`.


>Languages that implement error handling well do not communicate error states in the form of massive strings like ...

In other words, exceptions with a stack trace. It's funny how that pattern is being reimplemented because it's not available in go


Not funny, kinda disastrous ;)

The error interface's only member is `func Error() string`. If you want to do fancy things with errors rather than accumulate a massive string then there is absolutely nothing stopping you. As long as you include a `func Error() string` in there too then whatever you implement will be perfectly compatible with the std library and any other Go code that uses errors.


Not available? But that's exactly what Go's panic/recover system does.

IME, "retry after a delay" is often much more of an anti-pattern than "pass the error up the stack". That's how you get system's that take an age to do anything before eventually failing anyway (or worse, just leaving you with a perma-spinner). As a user, I'd much rather use a system that fails fast, and allows me to easily retry myself.

This is how the bank I used to work at ended up with app loads taking almost a minute. People don't intuitively understand the impact of transitivity and exponential growth. It is a very, very bad idea to make a habit of using retry-with-backoff as a default for error handling. (That's before we even consider the semantics of errors in stateful programs, the virtues of 'let it crash', etc.)

This. You have to pick a layer that is responsible for retries. If every layer does it, you get a failure amplifier that might become a death star laser.

Yup. We once analysed a particularly slow query (as in, a specific call to our server, not a path in the abstract) and saw that across the whole cluster it had resulted in eight million retries. And I think it was a 4xx in the end anyway (not that Go is designed for fancy stuff like telling the difference between two kinds of error!).

That's why you wrap errors, so you can unwrap them and work out what caused them.

But I totally agree that blindly retrying is a bad practice.


8 million! How on earth did that happen?

"And obviously, an even better pattern is to actually deal with the error" How would you deal with NewFromString? https://pkg.go.dev/github.com/shopspring/decimal#NewFromStri...

Try different strings? Maybe run through some AI code to figure out what the string really should have been? Most errors you just have to throw your hands up and declare something's wrong. I write in Go and Kotlin and Kotlin has been much less buggy because errors naturally flow up where they are logged and stop the normal execution. I've seen a number of examples in Go where the error was not returned, causing silent failures.


There's a ton of static code analysis tools for Go that check whether the error is actually handled/passed/etc - I'd say they're almost mandatory they're so useful :)

edit: The thing with NewFromString is that the string you're trying to convert to a decimal can't be. So yes, you're going to have to try a different string if you want this to work. Or you could deal with it a different way - maybe you could see if it converts to a float and then convert the float to a decimal?

And in the typical exception-using code base I've seen, there's one try block at the bottom of the stack with a catch block that throws whatever error message it has at the user. Anything is better than this.


There's a ton of static code analysis tools for Go that check whether the error is actually handled/passed/etc - I'd say they're almost mandatory they're so useful :)

I find this slightly at odds with the argument (made by other people, not you) that in Go you’re not reliant on the IDE, because all the program behaviour is immediately visible.

I don’t really understand the objection to using an IDE to look up overridden operators and so on. It seems like it’s in the same category as having a type-checking compiler that knows how to look up a type definition and check that you’re using valid operations. There’s no way all the information you need is going to be nearby in the text in any reasonably-sized program.


I work in the banking industry so I can't just try different strings until it works. These strings might represent real money. If it can't be parsed then something is screwed up. The program is reading the wrong file, the data is corrupt but my point is, there is nothing the program can do to fix it. The program simply needs to throw or log and error and someone needs to research what happened. Here's another example:

// NewV4 returns a randomly generated UUID. func NewV4() (UUID, error) { return DefaultGenerator.NewV4() }

You want a UUID but it might give you an error. What do you do except also return an error?


using `var err error` at the top is a sensible solution, I'm surprised this isn't the norm. As for dealing with the errors, there is value in wrapping an error with a message at the source of the error, but much of what follows is just passing the error up to whichever function is responsible for handling it (which may be, as you say, retrying after a period), analogous to rust's `?` operator. There is a proposal for defining a general error handler (see https://go.googlesource.com/proposal/+/master/design/go2draf...) but iirc that's in limbo at the moment

> A better (but still not perfect) pattern is to wrap the errors

So manually reinvent exceptions :-)


Wrapping errors is not the same as exceptions. At all. Even vaguely.

You don't get stack traces with wrapping errors, which means exceptions are still strictly superior.

Again, you're confusing two different things. You can still wrap error messages and have a stack trace.

Which have to be done manually or through code gen. So we're back at exceptions, but with the downsides of verbosity, and ability to forget to propagate them. Basically inferior exceptions in practically every way.

When dealing with go errors I like to give them proper names. barErr, fooErr. In the case of the article it fixes reordering (not that you do that a lot in real code). In real code it makes unhandled errors a compile error, because the variable is not used.

That’s a great idea. I’m going to try that.

Ever since I started writing Go professionally I've gotten into the habit of annotating errors in other languages as well. The difference between Go and Haskell is then mostly one of a few characters and some syntax differences. But conceptually it's exactly the same.

Take the error returned by some function call, add a bit more context so I can differentiate this XYZ error from the others, then pass it along to the caller.

I really couldn't care less about the details of how that happens and I find it puzzling that someone would write such a lengthy blog post about what is to me a completely trivial matter.


Other languages have stack traces, so you don't have to reinvent the wheel in an inferior manner.

Error handling isn't that verbose in Golang compared to other languages. Sure, in Python or Java(script) you can just throw an exception and handle that in a try/catch loop, but IMHO that's worse than explicit error handling, and I don't think that it produces more legible code in general. And error handling that's baked into the type system (as e.g. in Rust or Haskell) also requires pattern matching to handle errors, which really isn't that different from Golang's "if result, err := func(); err != nil { ...} else { ...}" pattern.

You mentioned Rust: well, Rust has the question mark operator for this case, so you can automatically unwrap a Result<T, E> and return the Err<E> with only one character of code. I worked with Go for years, and it's just not true to say that Go's error handling is not unusually verbose.

Maybe that's just me, but I find the explicit returns easier to read.

A thing that helps is that syntax highlighters normally color the question mark differently, helping it stand out

Passing just the error up the stack is also quite easy in Golang, just use a named error return value and assign to it:

    func test() (result interface{}, err error) {
        _, err = someOtherFunction()
    }
The thing is, you probably want to handle the error eventually, and that is just as verbose in Rust [1] as it is in Golang (IMHO). One can of course debate if it's more pleasing to use a "match" operator than an if/then/else loop to handle errors, but to me the difference is marginal. BTW one could even mimick Rust's type-based errors in Golang by returning a single interface{} type that's either a result or an error and then using a type switch, but that seems just silly and I have never seen that in the wild.

1: https://doc.rust-lang.org/rust-by-example/error/multiple_err...


A stack of ten functions wants to handle the error once. Returning it nine times is exactly the type of mind-numbing chore that computers do much more quickly and reliably than people, and I will never understand why so many want not to automate this.

Returning an error through nine levels of functions without doing anything with it anywhere in that call stack would indicate a strong problem with the code architecture to me. Personally I rarely pass on errors "as they are" in Golang anymore, as that makes debugging quite hard. A function receiving an error should either handle it (e.g. by recovering from it or logging it), or at least add relevant context to it so that the user can figure out what's wrong.

I used to like passing errors up the stack like you describe, but this invariably leads to hard-to-debug programs that terminate with unspecific errors like "i/o error", leaving it to the user or developer to figure out in which exact branch of the 9-layer call stack that error originated. If your error handling is exception-based and if your interpreter/runtime generates tracebacks you can do that, but not if you explicitly handle errors within the regular control flow.


Wrapping at every layer usually ends up with:

  failed to start a session: failed to read the session config file: i/o error
This is perfectly equivalent to

  java.lang.IOError
  in startSession() line 192
  in readConfig() line 123
  in openFile() line 131
Except that with Exceptions you get this for free, with more context than naive wrapping (and much more than `if err != nil { return err }` ).

Usually with exceptions, you want to catch and add more context at every layer boundary, and get the call stack for free.


The only thing I'm missing there is the filename, otherwise this is a helpful error message since it tells the user what the problem is and at which stage the error occurred. Stack traces, on the other hand, are mostly only helpful to the original developer(s) of a program, they won't help most ordinary users. And while they of course include the calling context they usually don't include other crucial information like variables (e.g. the filename in the example above).

Also, raising and catching exceptions is expensive in many languages. I know that in Java it's fast, but other languages like Python produce significant overhead when creating and raising an exception, that's something one should consider as well.


All you're arguing, then, is that regardless of whether you use error values or exceptions, you need to write good error messages. Hmm, okay. I agree with you on that. But I don't see how that counts against exceptions. Do you think the error message of an exception cannot contain filenames and variable values? And even in cases where they don't (e.g. you're depending on an external library with poor error messages), do you think it's impossible to add context to an exception's error message after it's been raised?

Also, the performance argument counts against your point, not for it. Exceptions rarely happen (when used properly), and when they are not raised, they cost nothing. Whereas go-style errors require explicit checks every single time you execute an operation which can fail.


> this invariably leads to hard-to-debug programs that terminate with unspecific errors like "i/o error", leaving it to the user or developer to figure out in which exact branch of the 9-layer call stack that error originated

You might want to add a caveat to 'invariably', something like: 'provided you're using a language where errors are modelled as one big mashed-together string' ;)

If we assume the basics of how Go is designed are an invariant, I think the best change would be adding syntactic sugar to return a struct satisfying the error interface, annotated with the (statically encoded) symbols and line numbers, and then also the arguments for those function calls. I have never seen a Go codebase whose hand-written 'contextual' error handling blocks provide even this, let alone more useful information. If you have, I'd be interested to hear about it!


Since Go 1.13 wrapping errors is part of the standard library [1]. You could use the "runtime" package to annotate errors with information about the calling context, but IMHO that's not a good idea. I think stack traces should be reserved for unexpected errors, and that is already well covered by the "panic" mechanism. Potentially expected errors that can be handled or returned to the user should simply provide enough information to know what went wrong without forcing the user to go through the code.

1: https://go.dev/blog/go1.13-errors


I see value in wrapping the error at the source, but I don't see the value in wrapping it beyond that point, unless there's there's genuinely new context to add at some boundary.

Now that computers are starting to write software it will eventually matter less. Until then, code is written by and for people, not computers.

The difference is when a function has more than one line.

In go, you constantly have to `if err != nil { return }` even with named return values. This is the part that breaks up the flow constantly and hurts readability.

In Rust, the ? macro also handles this.


Precisely. As soon as you go beyond a ten-line toy program, you'll hit this problem, and in my experience it's almost universally acknowledged to be a problem with Go.

Rust's '?' sugar effectively replaces the whole `if err != nil { ... }`, meaning it wasn't really honest to list Rust as another language that proves that error handling just has to be verbose and repetitive.


See my comment below. I never use this pattern personally, just wanted to point out that it exists. In general I think passing up errors through the call stack without adding context to them or handling them is a strong anti-pattern. I think many people in the Golang community see this similarly. And by passing an error up the stack with "?" you haven't magically "handled" it, so I don't think that's a fair point.

I'm not sure how that code block is meant to work. You still need to check the value of error and conditionally return, unless you're suggesting that most functions only contain one function call.

I see a symmetry between return values that you are required to check, checked exceptions (that you therefore have to catch), and patterns that you have to match. In each case, the compiler forces you to handle your errors (or, more generously, the compiler helpfully points out where you failed to do so).

But I think that pattern matching, and even checked exceptions, are much less verbose than constantly checking the return value. That doesn't mean that they're better, just less verbose. (Exceptions in particular - I can invisibly leave this function at points that are not specified. Does that leave everything in a clean state in all possible circumstances? Explicit error returns are much more verbose, but may be easier to reason about, because the information you need is all explicitly visible.)


> But I think that pattern matching, and even checked exceptions, are much less verbose than constantly checking the return value. That doesn't mean that they're better, just less verbose.

I don’t see how you can say that monadic results aren’t strictly better: they’re safer, less verbose, and give the option of abstracting around. Odds are they’re even more efficient.


try/catch can be as much as verbose

    try {
        foo();
        val = bar();
        baz();
    } catch (Exception $e) {
        // all lumped together, not good
    }
    
    try {
        foo();
        val = bar();
        baz();
    } catch (RangeException $e) {
        // who is throwing what?
    } catch (NotFoundException $e) {
        // who is throwing what?
    } catch (SomeOtherException $e) {
        // who is throwing what?
    }
    
    try {
        foo();
    } catch (SomeOtherException $e) {
        // very verbose
    }
    
    try {
        val = bar();
    } catch (NotFoundException $e) {
        // very verbose
    }
    
    try {
        baz();
    } catch (RangeException $e) {
        // very verbose
    }

That’s only if you’re actually handling your exceptions. The author of the post isn’t handling errors he’s just propagating.

When just propagating, exceptions have no verbosity:

   foo();
   bar();
   baz();

I don’t think “in the worst case scenario imaginable exceptions are as verbose as Go’s baseline” is much of a slam dunk.

Wow, that's an excellent illustration!

This person just does error checking, not error handling. Every single example in this post is a bare `return err`. At least wrap the error like `return fmt.Errorf("could not do foo: %w", err)`.

wrapping the error at the source is sensible, but if you need to pass the error five levels up before reaching the function that's actually responsible for handling it (e.g. retrying after a period), I find the wrapping becomes superfluous and further obscures the intention of the function. Rust's `?` operator works well here.

There is no categorical presumption in Go that an error invalidates what would be boxed in a Result or Option.

I do think that what seems obviously true and categorical in the absence of concurrency is quite a bit more subtle when concurrent. It’s obvious when rewriting the kind of chains where ‘?’ makes perfect sense from sequential to concurrent invocation.

By no means do I think Go is perfect or can’t learn from Rust but I do think it’s important to consider the tradeoffs against Go’s colorless concurrency - it’s a very compelling affordance. Like garbage collection it’s easy to show the pros and cons in a codebase the size of a demonstration but at scale it’s a decision with pros and cons.


Note that this can quite problematic in scenarios where the caller wants to inspect the error (example: was the error an http timeout? or a server error?). Returning the error and letting the topmost call site handle that is usually better.

That's what `errors.Is` is for. https://pkg.go.dev/errors#Is

It would be interesting if anyone used error types in Go, but you'll be hard pressed to find libraries that do. return fmt.Errorf() all the way.

In my experience, errors returned by the standard library and recent third party packages are fairly well discernible, although not in all cases.

Note that you don't need custom error types, just exported Error instances. These were in use even before wrapping was introduced with Go 1.13 (2019); one had to compare or type-assert directly instead of unwrapping.


You can use %w in fmt.Errorf to accomplish this.

Not really. If the original error is `fmt.Errorf("divide by 0")`, no amount of `fmt.Errorf("failed to divide: %w")` will help anyone programmatically recognize the error.

Yup, fair point, I thought we were talking about libraries replacing errors to obscure the original, which absolutely happens. Lots of libraries (e.g aws-sdk) do use typed errors, so wrapping them correctly for smaller libraries that compose them is important.

No you should use errors.Is(err, http.ErrHandlerTimeout) instead of simple equality.

Thanks (and to mseepgood as well). TIL :)

The more I've used golang, the more I've had a creeping suspicion that 'the emperor has no clothes'.

Go plays at being a system programming language, but it's really not. It's peers and competitors are languages like java and c#, not rust or c++. When you look at it from that lens, it fares badly. There's just a clunky feel that permeates the entire language, and it's creators seem to have no interest in addressing it.

I've said it before, and I'll say it again. Golang is popular because it was written by Thompson, and embraced by google. You have a whole load of devs that will simply embrace anything with such a pedigree because it 'must be state of the art'


> You have a whole load of devs that will simply embrace anything with such a pedigree because it 'must be state of the art'

Dude, at the end of the day I want something done. And golang has good libraries (esp network stuff), good IDE support, and compiles to single binary that's reasonably fast and easy to deploy.

Show me another language that covers all these.


The java and .net ecosystem come to mind if you're willing to have the java or .net runtime on your container (I never understood the big deal about binaries outside of embedded or other space constrained environments)

Command line tools etc.. for some types of servers too, Go has the advantage of less memory usage and faster startup. That said, for internal web apps or websites etc.., PHP / Django / spring boot whatever sails the boat.

The startup time has interesting implications for serverless. That was the big draw of golang for me. Cold boot time for a golang function is like half a second max.

For CLI tools as well. And Golang stdlib feels more batteries included and straight to the point than Java counterparts, in many niches.

> You have a whole load of devs that will simply embrace anything with such a pedigree because it 'must be state of the art'

That seems rather dismissive of people. The devs I know are not sheep.

Now if you had said that we have a whole load of managers that will simply embrace anything because it 'must be state of the art'...


You're living in a bubble. People get reliable stuff done in the real world with it. And fast.

One could say the same about plenty of awful programming languages.

If the blog post was titled "Go is unusable in production" then sure, he'd be wrong. But he's simply criticizing the language design. (Mostly, I might add, in the hopes that it will someday improve; the author writes Go professionally, he's not just some random hater.)


It's not mutually exclusive with what he wrote. I have used golang for several years now and he's on point 100%

I understand the criticism from the author, but he missed a simple case to make the flow a bit better when a function returns a value and an error:

From the article:

if val, err := bar(); err != nil { // ERROR: val declared but not used return err } fmt.Println(val) // ERROR: undeclared name: val

If you want to use the val after the if, you could use it in an else:

if val, err := bar(); err != nil { // ERROR: val declared but not used return err } else { fmt.Println(val) // this is ok }


I’d love discussions on programming languages to be done by people who know various languages with different paradigms. I feel a lot of the discussion is often around things that were in haskell for more than 20 years and we understand them and know how to handle them (in this case the Either monad). There should be a dumbed down version of Haskell that doesn’t make use of any of the math lingo in its documentation and maybe uses curly braces to make it more approachable to young-bloods.


Something like this but for server side.

What you're looking for is OCaml: https://ocaml.org/

Yeah like OCaml, just without global lock, modern and cool and with fast compilation (can be at the cost of some convenience features). Ideally interop with something that has all the libraries. And without the imperative and object oriented parts.

Like Rust?

Like rust, but purely functional and garbage collected.

In my language (neat, but don't use it yet, it's pre-alpha), you can propagate error types by picking out the valid return type. For instance, if a function returns `(string | ErrorType) foo()`, you can declare a result variable as `string a <- foo();` ("pick `string a` from `foo()`") and it will automatically do "if (error) return error;" to handle the other case.

Go could easily do that too if it weren't as afraid of "complex" features.


Many people criticize about the verbosity of Go's error handling — which I'm not a fan of, but I can still live with it — but no one discusses about a problem which I think more fundamental: It's too easy to ignore errors in Go.

In exception-based languages, if you don't handle an error, it will be bubbled up and possibly kill the whole program. Similarly, in Rust if you handle an error "lazily" by `unwrap`-ping it, it will possibly terminate the entire program. In these languages, if an error happens in line X and it's handled "lazily" or even not handled at all, line X + 1 won't be executed. Not in Go.

Ignoring errors might be okay if the zero value returned when there's an error is expected by the caller. For example:

    // If the caller expects the default value of `count` is 0, this is fine
    count, _ := countSomething(...) // return (int, error)
However, in many cases the zero values are garbage values because the caller is expected not to use it if there's an error. So, if the caller ignores the error, this can be a problem which may lead to a very subtle bug which may cause data corruption/inconsistency. For example:

    user, _ := getUser(...) // return (User, error)

    // If there's an error, `user` will contain the zero value
    // of `User`: `{"Id": 0, "Email": "", "Name": "", ...}`, which is garbage.

    // So, if there's an error, the next line, which assumes there's no error returned by `getUser`,
    // may lead to a subtle bug (e.g. data corruption):
    doSomething(user) // Oops if `user` is a zero value
This is partly due to Go's weak type systems (no sum types) and partly due to Go's dangerous-and-may-lead-to-a-subtle-bug concept of zero values.

Someone might argue that good programmers shouldn't ignore errors like this. True, but good languages should be designed such that bad practices should rarely happen, or at least require more conscious effort. For example, to do similarly to the previous example in Python, you need to write:

    try:
        user = get_user(...)
    except:  # Catch any exception
        user = User()

    do_something(user)
In Rust, you can do:

    let user = get_user(...).unwrap_or(User::new());
    do_something(user);
In both languages, because there's no concept of zero values, you need to explicitly set a fallback/default value. While I understand why Go needs the concept of zero values (it treats errors as values but it doesn't have sum types), I think it does more harm than good. If a language treats errors as values, it'd better have sum types.

In other languages you have these endless try/catch things and then the error is all the way at the bottom away from the actual problem, plus it feels kinda hacky.

I don't get why folks go through such mental gymnastics and contortions to "think" that this strewn through their code bases is a good idea (or at least a slightly positive aspect of the language):

    if err != nil {
            return err
    }
No language is perfect and not all are an improvement on languages that have come before. Just admit it's rubbish and move on.

That's exactly what people do. Nobody likes iferr all over their code, but every other way is worse.

Admit and move on is the status quo.


Rust handles this quite nicely, with the `?` operator, which someone already mentioned in the comments. It's still as explicit as you want it to be, but made real easy by that small amount of syntactic sugar. Maybe Go should adopt something similar.

I agree with this sentiment, though I am glad that Go does not fall into the "throw/try/catch" trap and instead treats errors as variables.

I think some sugar around this would be uncontroversial.


> I want to live in a world where the function looks like this (...) Here the question mark tells us that if the function returns an error, we should return that error (...)

Ever heard of exceptions?

This is exactly what exceptions are made for. To allow you to write simple code but still make sure errors are propagated through your code.

It also makes it easier to write code that has to run something regardless of an error (using try/finally), while still ensuring that error is passed through your function reliably.

The question mark isn't necessary at all. Why would you pollute your code with additional character? It just says "I want to have a broken application that does not react to an error if I forget to put in a question mark"


Not GP.

I think a great article detailing why exceptions are not desirable is http://www.lighterra.com/papers/exceptionsharmful/ .

> The question mark isn't necessary at all. Why would you pollute your code with additional character? It just says "I want to have a broken application that does not react to an error if I forget to put in a question mark"

I'm no Rust expert, but I think the compiler will error if you don't put the question mark?


What is going wrong in the Golang community that Duffield, who is a language designer and implementer, does not know about gofpher <https://news.ycombinator.com/item?id=25648031>?

Not a rhetorical question; I really want to find out. Hackers, respond with your insights and speculations.


In 2019, Russ Cox acknowledged three of the most commonly reported Go pain points:

> The top three pain points for Go users, in surveys and direct feedback, have been consistent for a number of years. They are: package management, generics, and error handling. We are working on all three.

The first has been solved, the second is in the process of being solved, and the third has been addressed in two major proposals, both of which were rejected. I sympathize with the author's frustration, though I would argue that better error handling in Go is still being actively discussed and investigated.

https://twitter.com/_rsc/status/1146129898383302656

https://go.dev/blog/go2draft

1. Package management has, more or less, been solved through minimum version selection in modules/vgo. Though not everyone's favorite, at least it doesn't require a SAT-solver (dependency hell is NP-complete https://research.swtch.com/version-sat)

https://github.com/golang/go/issues/24301

https://go.googlesource.com/proposal/+/master/design/24301-v...

https://research.swtch.com/vgo

https://golang.org/ref/mod

https://github.com/golang/go/wiki/Modules

https://go.dev/blog/using-go-modules

https://golang.org/doc/tutorial/create-module

2. Parametric polymorphism/Type Parameters ("generics") is/are being introduced into the language in 1.18, which is slated for release in early 2022.

https://github.com/golang/go/issues/43651

https://go.googlesource.com/proposal/+/master/design/43651-t...

3. There have now been a couple of proposals to make error handling simpler and reduce boilerplate

https://github.com/golang/go/wiki/Go2ErrorHandlingFeedback

check/handle

https://go.googlesource.com/proposal/+/master/design/go2draf...

https://go.googlesource.com/proposal/+/master/design/go2draf...

try

https://github.com/golang/go/issues/32437

https://swtch.com/try.html

https://go.googlesource.com/proposal/+/master/design/32437-t...

https://news.ycombinator.com/item?id=20339697

https://news.ycombinator.com/item?id=20100902

https://news.ycombinator.com/item?id=20454966

related

https://go.googlesource.com/proposal/+/master/design/go2draf...

https://go.googlesource.com/proposal/+/master/design/go2draf...

https://go.googlesource.com/proposal/+/master/design/go2draf...


Why is Go syntax fugly though? I don't have anything against the language and it's actually on my study plan soon but jeez the syntax looks very unappealing to me.

How so? Missing parenthesis for if-statements and such? Looks like a mix of Pascal and C, missing parenthesis but still have curly braces.

It could be this blog post. No, I actually like no paranethesis for ifs (just like in Ruby). Maybe the rather alien := thing, I just need to get used to the language I guess, but I didn't get this nice feeling of elegance I got when I first looked at Ruby or Kotlin when looking at Go. I know it's completely subjective though.

I completely agree with you. I tried Go a few months ago and I was really surprised by its performance, build sizes and how easy is to write concurrent stuff, but the syntax really did throw me away. It's probably something a bit superfluous, but coming from Rust it's hard to enjoy Go's syntax.

the designers of Go are all having Unix background (Rob Pike, Ken Thompson), and they need a language that is simple and stupid just like Unix, and so now you know how painful Go is, just like how painful Unix is when everything is text and files

Someone needs to write a proposal to make the program play the tune from Stayin' Alive whenever a function returns an error. That way the Go devs can still pretend it's the 70s, meanwhile the rest of us can sneak in some usable error handling underneath.

> This bloats functions and obscures their logic

I start to enjoy those developers' cries realizing that error handling is part of the logic too. Sometimes even more important part.

Great job, Go.


"Please respond to the strongest plausible interpretation of what someone says, not a weaker one that's easier to criticize. Assume good faith."

https://news.ycombinator.com/newsguidelines.html


I think the point is that while error handling is part of the program logic it is not necessarily part of every functions logic. Sometimes you just want to pass an error up the stack to be handled at some other error boundary. This is what Rust (with the ? operator and Result type) and functional effect libraries (Scala ZIO or cats-effect) provide. You can ensure at compile time that errors are handled somewhere but each individual function in the call stack can decide whether it wants to explicitly handle an error or just pass it up the stack. And if you just want to pass it up the stack then there is no syntactic overhead associated with it.

> Sometimes you just want to pass an error up the stack to be handled at some other error boundary.

Right, but I think the idea is that the programmer should think and decide every time whether that's what you want to do. I haven't programmed in Rust, but I suspect that people use the ? operator more than is ideal because it's so easy.


Probably so, but I think as long as you ensure the error gets handled somewhere (rather than crashing the program) it is good tradeoff to make. That was the problem (IMHO) with unchecked exceptions in Java. You make everything an unchecked exception because constantly adding code to handle checked exceptions is annoying and verbose, but then if you don't handle the unchecked exceptions somewhere then they crash the JVM. This is where I think the Scala/Rust approach is best. The compiler can force you to handle the error somewhere but there is very little syntactic overhead if you want to ignore an error in any particular function.

> And if you just want to pass it up the stack then there is no syntactic overhead associated with it.

"Errors are values" means that this situation is not very different from "I just want to pass the result of my sqrt function up the stack" :) By reading code of function that returns the result of computation the next pair of eyes that gonna read this code will clearly see the intent and idea behind the code. The same should be with "unhappy path" – if intent was just to pass error, without caring of annotating it or doing something with it – it should be as clear and explicit as possible.

There is another benefit of "syntactic overhead" – incentive to improve the error handling code. I.e. I may not care at the beginning about annotating error, and just use "return err", but later on I want to make error messages more useful, so the "if err != nil {\n return err }\n" structure makes it easy to annotate it. Instead, if Go code was riddled with "?"s or "!"s or whatever syntactic sugar other use, I would not be so eager to switch from "concise and short single-char" to the "bloated 3 line".

Incentives matter in coding psychology. I wish more research was done on that.

Bottom line, hiding error handling has more long term drawbacks than benefits.

PS. I virtually never use "return err" anymore. Always at least annotate the error – it helps error messages readability immensely without resorting to adding wasteful stacktraces.


This piece is both good and original. However...

Isn’t there a Rob Pike quote for these?

"Who cares? Shut up"

It’s in the context of concerns from newer developers who don’t focus on what’s important.

This is the same blog that gave us code smells, abstract more to hide it (https://jesseduffield.com/Type-Keys-Revisited) -- only to point out the possibly differing perspectives on software development.

There’s a philosophy to the language that’s very clearly defined. You have many options these days on what you write in. You’re not changing how Go does error handling because you think it’s needlessly verbose.


That definitely sounds like Rob Pike. As for changing how Go does error handling, there is actually a proposal in motion to do exactly this, with support among both devs and the language maintainers: https://github.com/golang/go/issues/21182#issuecomment-54241...

That looks like lukewarm support at best, on something that hasn't moved beyond an issue with little details, and is closed.

I would not represent that as anything else. The entire process is captured here: https://github.com/golang/proposal


I'm not sure what you mean when you say closed: the proposal is clearly open, and the latest comment asking if anybody has issues with the proposal has no downvotes. That sounds promising to me.

At the very least, it's clear that the language maintainers see an issue with the current error handling, given how much time they've spent working on proposals. The community also clearly cares: see https://twitter.com/_rsc/status/1146129898383302656

Also, I see you amended your original comment with a jab against me for the type keys post. Not sure what to tell you there, I posted something, absorbed the feedback, and incorporated that into the blog. If you have specific issues with the latest post please let me know.


> Also, I see you amended your original comment with a jab against me for the type keys post.

I didn't realize you were the same person who wrote that blog post, and I edited it to include it when I realized. I linked your amended version, not your initial version if that's of any worth.

And you're correct it's not closed. I still stand by that particular proposal is going nowhere. You can make any proposal you want. It hasn't even moved to the design stage yet in 3 years, so I'm not sure why it's something you hang on to as a signal error handling is changing. Take generics for example, and the years (decade) of work that it took.

This is unsolicited blog feedback:

Your blog and writing style doesn't read like it comes from a place of humility and learning, but from a place of authority. Sometimes that's OK, but in your case, feels unwarranted. And the constant push to get it on to HN or Reddit or N other platforms so it can spread... Thought leadership as an aspirational goal has never been something I look for in blogs I read (I recoil and go elsewhere when I sense this is the goal).

I understand it's hard work, and you want others to see it, but just someone else's perspective.

Some blogs that I really enjoy (and I hope others emulate so I'm sharing):

- http://neugierig.org/software/blog/

- https://brooker.co.za/blog/


Thanks for the feedback: I've found that most of the learning I've done has been thanks to feedback from reddit/hackernews so I'm very much dependent on it to know whether I'm off the mark. In terms of authoritative tone, I agree: some posts are stated in confident terms because I _am_ confident, but of course I'm learning to adjust that confidence level based on feedback.

It does apply though. If you think error handling in Go is too verbose or boring, you're not solving a real problem. If you're trying to come up with design patterns and strategies - as described in the article - to see if you can shave off some code (at the cost of complexity and non-idiomatic code), you're not solving a real problem.

Software is never about the low level nitty gritty, and in these cases it's better to stick with what people expect to see / read. Don't make people think. I'm not smart enough to read your interesting error checking.




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

Search: