Structured errors in Go
19 points by runxiyu
19 points by runxiyu
Hot take. But if you squint a bit, errors are basically contexts but they operate on the ascending side of the call tree, not the descending side.
Contexts accrue information as they descend the call tree and errors accrue information as they ascend the call tree.
This means that, if you want to store structured metadata in a context, that information is only useful if you pass it to an error so that metadata can ride the call tree ascension like a rollercoaster all the way up to the top where it’s actually useful.
This was the most thought-provoking part of the article! It reminds me of how a Zig style Error Return Trace are more useful in figuring out problems in how an expected error was handled (flow control) and surfaced to the user.
oh it’s me!
we’ve been using these ideas/library in prod for a few years now, some rough updated thoughts:
luckily i kept it below v1 so at some point i’ll break the api and finish all those improvements i’ve been wanting in prod!
We converged on more or less the same ideas at $WORK (where we also write an HTTP API in Go), it’s validating to see we are not the only ones!
The only difference is that when we’re attaching the context to an error we’re also attaching a stack trace to the error and as a result don’t bother with wrapping errors.
I’ve seen similar ideas in otel style tracing frameworks that sort of mix together all the key-value attributes for a trace into both logging and errors, we didn’t have a need for tracing so I wanted to borrow those ideas for simpler applications.
i did toy with avoiding wrapping and instead using some form of contextual state for a given call tree, but the wrapping mechanism while not performant (essentially a linked list) worked out simpler and easier to make concurrency-safe. another thing i ran into was while you can sort of treat a context chain as wholly stateful, it wasn’t entirely idiomatic, the general consensus fell on contexts being additive down the call tree and subtractive up the call tree (in other words, where the context was originally created, it should be impossible to access data from a deeply nested context from earlier in time/deeper in the call tree) which i did abuse a little…
i did toy with avoiding wrapping and instead using some form of contextual state for a given call tree
I tried exactly the same! I don’t remember the exact details, but we actually implemented it, tried it, and decided to go back to immutable contexts (concurrency was not the reason).