Live data from Hacker News

Structured Errors in Go (2022)

southcla.ws

61–70 of 75 posts

Re: Structured Errors in Go (2022)

#61

The implementation of WithMeta() is flawed. Not only is it not concurrency-safe, every nested call will be modifying the parent map. The way to do this in a safe and performant manner is to structure the metadata as a tree, with a parent pointing to the previous metadata. You'd probably want to do some pooling and other optimizations to avoid allocating a map every time. Then all the maps can be immutable and therefo…

I like this, as it also maps well to using defer at the top of the function to wrap any returned errors with metadata.

I've implemented something similar in my errors library relying on log/slog.Attr.

Re: Structured Errors in Go (2022)

#62
post #52

Earlier quoted context omitted.

It's not that the example is incomplete, it's that it's incorrect! :)

No, it's just incomplete. The same code just needs to unwrap: for err != nil { switch e := err.(type) { case UserNotFound: writeJSONResponse(w, 404, "User not found") return case interface { Timeout() bool }: if e.Timeout() { writeJSONResponse(w, 503, "Timeout") return } } err = errors.Unwrap(err) }

`err.(type)` is incorrect, at least in general. Calling `errors.Unwrap` in application code like this is almost always a red flag indicating a design error. And in this case it definitely is!

Re: Structured Errors in Go (2022)

#63
post #62

Earlier quoted context omitted.

No, it's just incomplete. The same code just needs to unwrap: for err != nil { switch e := err.(type) { case UserNotFound: writeJSONResponse(w, 404, "User not found") return case interface { Timeout() bool }: if e.Timeout() { writeJSONResponse(w, 503, "Timeout") return } } err = errors.Unwrap(err) }

`err.(type)` is incorrect, at least in general. Calling `errors.Unwrap` in application code like this is almost always a red flag indicating a design error. And in this case it definitely is!

That is literally what errors.As() does, finding a value up the cause chain that can be coerced to the target type.

Re: Structured Errors in Go (2022)

#64

Earlier quoted context omitted.

No, I want dedicated classes. Be they thrown or returned as a value. Error codes are limiting and serve a different purpose. Error codes contain only the type of error that occurred and cannot contain any more data. With an error class you can provide context - a 400 happened when making a request, which URL was hit? What did the server say? Which fields in our request were incorrect? From a code perspective, if an e…

You need some kind of grouping otherwise any simple action ie. involving single io would require handling of dozens different classes.

Yes of course. That's why I mentioned polymorphism. A FileNotFoundException and NoDiskSpaceException can inherit from IOException for example. With polymorphic error classes, a caller can decide if they want to handle the different cases individually, or just catch the overarching IOException.

All this flexibility comes for free when your use your language's type system, whereas with plain error codes you would have to implement grouping yourself manually with some kind of lookup table.

Re: Structured Errors in Go (2022)

#65
post #62

Earlier quoted context omitted.

`err.(type)` is incorrect, at least in general. Calling `errors.Unwrap` in application code like this is almost always a red flag indicating a design error. And in this case it definitely is!

That is literally what errors.As() does, finding a value up the cause chain that can be coerced to the target type.

If you're re-implementing errors.As's unwrapping behavior in your application code in order to parse/evaluate errors, that's a mistake and a design error. You'd never call Unwrap outside of a custom error type's method set, and even then you'd never use a `for` loop like you're doing in your example.

> it means you can target types with precision in the API layer

The only situation where you need to get precise error types is when you need to provide specific details from those specific types to the consumer, which is rare. And even in those rare cases, user code does that work via errors.As, not this manual Unwrap loop process you're suggesting here.

Re: Structured Errors in Go (2022)

#66
post #65

Earlier quoted context omitted.

That is literally what errors.As() does, finding a value up the cause chain that can be coerced to the target type.

If you're re-implementing errors.As's unwrapping behavior in your application code in order to parse/evaluate errors, that's a mistake and a design error. You'd never call Unwrap outside of a custom error type's method set, and even then you'd never use a `for` loop like you're doing in your example. > it means you can target types with precision in the API layer The only situation where you need to get precise error…

That is just your opinion. There is absolutely nothing wrong with doing so, and there is nothing in the documentation that asserts what you claim.

The documentation is clear that comparing an error value or casting it without following the Unwrap() chain is only an antipattern because it would not work with wrapped errors.

Is() and As() are merely convenience functions, and the documentation is clear that all they're doing is calling Unwrap(), which you can do yourself.

Re: Structured Errors in Go (2022)

#67
post #65

Earlier quoted context omitted.

That is literally what errors.As() does, finding a value up the cause chain that can be coerced to the target type.

If you're re-implementing errors.As's unwrapping behavior in your application code in order to parse/evaluate errors, that's a mistake and a design error. You'd never call Unwrap outside of a custom error type's method set, and even then you'd never use a `for` loop like you're doing in your example. > it means you can target types with precision in the API layer The only situation where you need to get precise error…

> The only situation where you need to get precise error types is when you need to provide specific details from those specific types to the consumer, which is rare.

It's not rare in my experience. All they apps I work on have a central unhandled error handler in the API that converts Go errors to HTTP or gRPC error responses, and then falls back to a general "internal error" if no specific error could be mapped. I can think of many other instances where we have a switch over half a dozen error typed in order to translate them into other types across RPC or pub/sub boundaries.

> And even in those rare cases, user code does that work via errors.As, not this manual Unwrap loop process you're suggesting here.

As() does not work with switch statements unless you pre-declare a ton (in our case, often a couple of dozen) error variables. Secondly, it is deeply inefficient. As() traverses the cause tree recursively for every single error, so if you have 30 possible error types to compare, and an error typically wraps 3 layers deep, that's a worst case of 30 loop iterations with 90 cases, as opposed to my method, which is 3 loops.

Re: Structured Errors in Go (2022)

#68
post #65

Earlier quoted context omitted.

If you're re-implementing errors.As's unwrapping behavior in your application code in order to parse/evaluate errors, that's a mistake and a design error. You'd never call Unwrap outside of a custom error type's method set, and even then you'd never use a `for` loop like you're doing in your example. > it means you can target types with precision in the API layer The only situation where you need to get precise error…

That is just your opinion. There is absolutely nothing wrong with doing so, and there is nothing in the documentation that asserts what you claim. The documentation is clear that comparing an error value or casting it without following the Unwrap() chain is only an antipattern because it would not work with wrapped errors. Is() and As() are merely convenience functions, and the documentation is clear that all they're…

It literally is not. Is and As are not merely convenience functions, they're canonical and conventional expectations of Go code, if you're doing that work yourself you're almost certainly doing it wrong.

Re: Structured Errors in Go (2022)

#69
post #65

Earlier quoted context omitted.

If you're re-implementing errors.As's unwrapping behavior in your application code in order to parse/evaluate errors, that's a mistake and a design error. You'd never call Unwrap outside of a custom error type's method set, and even then you'd never use a `for` loop like you're doing in your example. > it means you can target types with precision in the API layer The only situation where you need to get precise error…

> The only situation where you need to get precise error types is when you need to provide specific details from those specific types to the consumer, which is rare. It's not rare in my experience. All they apps I work on have a central unhandled error handler in the API that converts Go errors to HTTP or gRPC error responses, and then falls back to a general "internal error" if no specific error could be mapped. I c…

> t is deeply inefficient. As() traverses the cause tree recursively for every single error, so if you have 30 possible error types to compare, and an error typically wraps 3 layers deep, that's a worst case of 30 loop iterations with 90 cases ...

I have no idea how you came to this conclusion. It's certainly not what happens when you call errors.As in your application code.

There's no situation where your application code would ever have 30 error types to compare against, if that were ever the case you have seriously fucked up!

Re: Structured Errors in Go (2022)

#70
post #69

Earlier quoted context omitted.

> The only situation where you need to get precise error types is when you need to provide specific details from those specific types to the consumer, which is rare. It's not rare in my experience. All they apps I work on have a central unhandled error handler in the API that converts Go errors to HTTP or gRPC error responses, and then falls back to a general "internal error" if no specific error could be mapped. I c…

> t is deeply inefficient. As() traverses the cause tree recursively for every single error, so if you have 30 possible error types to compare, and an error typically wraps 3 layers deep, that's a worst case of 30 loop iterations with 90 cases ... I have no idea how you came to this conclusion. It's certainly not what happens when you call errors.As in your application code. There's no situation where your applicatio…

If you do this:

  var a, b, c error1, error2, error3
  switch {
    case errors.As(&a):
      ...
    case errors.As(&b):
      ...
    case errors.As(&c):
      ...
  }
…then yes, you will be doing 3 searches, each of which will do a loop (sometimes recursively if Unwrap() returns []error) over the chain of causes.

> There's no situation where your application code would ever have 30 error types to compare against, if that were ever the case you have seriously fucked up!

That is your opinion. In my experience, that is not the case, because there are lots of cases where you want to centrally translate a canonical set of errors into another canonical set.

> It literally is not. Is and As are not merely convenience functions, they're canonical …

This is just your opinion. If you actually read the documentation, you will see that it merely says Is() and As() are "preferable" to checking.

Post reply on HN