News for hackers. Stuff that matters.
Re:Go’ing Insane: Endless Error Handling by nicoburns on Tuesday September 14, 2021 @06:44AM (#28522739)
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.
Re:Go’ing Insane: Endless Error Handling by chrismorgan on Tuesday September 14, 2021 @07:06AM (#28522905)
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.
Re:Go’ing Insane: Endless Error Handling by thiht on Tuesday September 14, 2021 @07:11AM (#28522949)
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?
Re:Go’ing Insane: Endless Error Handling by nicoburns on Tuesday September 14, 2021 @07:48AM (#28523262)
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.
Re:Go’ing Insane: Endless Error Handling by samhw on Tuesday September 14, 2021 @08:27AM (#28523685)
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.
Re:Go’ing Insane: Endless Error Handling by politician on Tuesday September 14, 2021 @09:00AM (#28524069)
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()
  }
Re:Go’ing Insane: Endless Error Handling by nicoburns on Tuesday September 14, 2021 @09:13AM (#28524235)
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.

Re:Go’ing Insane: Endless Error Handling by chrismorgan on Tuesday September 14, 2021 @09:22AM (#28524390)
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.

Re:Go’ing Insane: Endless Error Handling by wavemode on Thursday September 16, 2021 @07:23AM (#28550326)
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.
Re:Go’ing Insane: Endless Error Handling by chrismorgan on Tuesday September 14, 2021 @08:00AM (#28523381)
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")?
Re:Go’ing Insane: Endless Error Handling by thinkharderdev on Tuesday September 14, 2021 @07:50AM (#28523279)
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.

Re:Go’ing Insane: Endless Error Handling by maffydub on Tuesday September 14, 2021 @07:42AM (#28523211)
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.

Re:Go’ing Insane: Endless Error Handling by ccakes on Tuesday September 14, 2021 @07:47AM (#28523252)
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
Re:Go’ing Insane: Endless Error Handling by brundolf on Tuesday September 14, 2021 @07:43AM (#28523216)
You can .map_err() before the ? to transform it inline using a closure
Re:Go’ing Insane: Endless Error Handling by Cthulhu_ on Tuesday September 14, 2021 @08:43AM (#28523868)
I don't know Rust myself but, where / how are the errors actually handled? Or is this like unchecked exceptions?
Re:Go’ing Insane: Endless Error Handling by chrismorgan on Tuesday September 14, 2021 @09:07AM (#28524163)
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.

Re:Go’ing Insane: Endless Error Handling by josephg on Tuesday September 14, 2021 @09:02AM (#28524095)
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).

Re:Go’ing Insane: Endless Error Handling by tialaramex on Tuesday September 14, 2021 @11:25AM (#28526254)
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).

Re:Go’ing Insane: Endless Error Handling by jamespwilliams on Tuesday September 14, 2021 @07:22AM (#28523024)
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.

Re:Go’ing Insane: Endless Error Handling by masklinn on Tuesday September 14, 2021 @09:15AM (#28524286)
> 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.

Re:Go’ing Insane: Endless Error Handling by kevinmgranger on Tuesday September 14, 2021 @10:04AM (#28524995)
> 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.

Re:Go’ing Insane: Endless Error Handling by gilbetron on Tuesday September 14, 2021 @08:58AM (#28524045)
Doing foo()? is equivalent to:

  err := foo()
  if err != nil {
    return err
  }
Re:Go’ing Insane: Endless Error Handling by sagichmal on Tuesday September 14, 2021 @07:54AM (#28523316)
The ? operator permits chaining, which obscures sad path control flow and makes code harder to reason about, not easier.
Re:Go’ing Insane: Endless Error Handling by toolslive on Tuesday September 14, 2021 @06:57AM (#28522822)
exactly! a little sugar would be most welcome.
[dead]
Re:Go’ing Insane: Endless Error Handling by piaste on Tuesday September 14, 2021 @09:16AM (#28524304)
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.

Re:Go’ing Insane: Endless Error Handling by zozbot234 on Tuesday September 14, 2021 @09:22AM (#28524386)
> 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.

Re:Go’ing Insane: Endless Error Handling by afranchuk on Tuesday September 14, 2021 @09:48AM (#28524730)
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.

Re:Go’ing Insane: Endless Error Handling by zozbot234 on Tuesday September 14, 2021 @09:53AM (#28524809)
> 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".

Re:Go’ing Insane: Endless Error Handling by tialaramex on Tuesday September 14, 2021 @12:20PM (#28527058)
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.

Re:Go’ing Insane: Endless Error Handling by Zababa on Tuesday September 14, 2021 @03:46PM (#28530041)
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".

Re:Go’ing Insane: Endless Error Handling by afranchuk on Tuesday September 14, 2021 @12:24PM (#28527108)
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.
Re:Go’ing Insane: Endless Error Handling by jesseduffield on Tuesday September 14, 2021 @09:21AM (#28524363)
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