I'm warpfork.
github: github.com/warpfork
twitter: twitter.com/warpfork
slides: slides.com/warpfork
{"error":{
"code": "error-codes-matter",
"message": "messages matter too",
"details": {"extensible": "yet simple"}
}}
You need to log them.
You need to show them to users.
You need to know your programs handle them gracefully.
JSON.
Incrementally legible.
Mechanically manageable.
Specifies the minimum bits you need; expandable.
- Errors need a code.
- It should be human-readable.
- It should search-engine'able.
- It should be machine-matchable.
And that's kinda all you need.
Everything else is icing on the cake.
switch err.Code() {
case "warpforge-error-network":
return retry()
default:
return value
}
// Good documentation should
// describe error codes!
//
// Errors:
//
// - examples-error-foobar -- comes from DoSomething.
// - examples-error-discombobulated -- also comes from DoSomething.
func DoMoreThings() error {
return DoSomething(nil)
}
- Errors need a code.
Treating this like an enum is ridiculously powerful, while still being compellingly simple.
Static analysis tools can help you a lot with just this much structure. (More on this later.)
switch err.Code() {
case "warpforge-error-network":
return retry()
default:
return value
}
// Good documentation should
// describe error codes!
//
// Errors:
//
// - examples-error-foobar -- comes from DoSomething.
// - examples-error-discombobulated -- also comes from DoSomething.
func DoMoreThings() error {
return DoSomething(nil)
}
- Freetext messages are nice:
- Easy to write
- Easy to read
- ... but not great for machine parsing. (So use the code for that.)
Serum says: use a string.
(Simple.)
Make it for humans.
return SerumError{
Code: "myapp-error-foobar",
Msg: "error handling workspace"
+"at %q: %s" % wsPath, cause
}
... so let's just have a map for that.
A key-value map is enough.
It's probably a good idea to repeat the details in the prose, too -- if we want end-users to see those details.
return SerumError{
Code: "myapp-error-foobar",
Msg: "error handling workspace"
+"at %q: %s" % wsPath, cause,
Details: {
"workspacePath": wsPath,
"otherdetails": whatever,
},
Cause: wrapErr(cause),
}
Stack traces are also details.
I know that seems rough,
but stack traces aren't really standardized. They're freetext.
So treat them like freetext.
What if I want to start using Serum...?
... Great! Do it!
What if there's no libraries in my language yet..?
... Make one! :D
It's just JSON!
What if I want to extend Serum...?
... Great! Do it!
(Adding fields to JSON is generally not a compatibility problem...)
Positive Experiences:
Negative Experiences:
Find it at
They're enumerable.
it's seriously so bad.
it's the #1 reason I start having trouble wrangling large codebases in golang and producing reliable software.
the error handling.
Looks roughly like this.
It's still very normal golang code.
...Notice the docs!
It's a pretty simple annotation style:
Errors:
dash
{code}
dashdash
{describe}
Codes returned by this function
Codes returned by this function
Now diff that with what the docs strings say.
Report any divergence.
Multiple return sites separated by any kind of branching work, as you'd expect:
Assignments are traced:
In finding an error type's code, branching is also understood:
Error codes can even be composed:
(as long as it's constant composition)
When things get too wild, overrides are an option:
Partial overrides? Yes.
Very Useful when using switch statements to reduce the taint spread!
Can you restrict the errors returned by interfaces, and will this result in their implementers being checked?
Yus.
When adopting go-serum-analyzer in even a medium codebase...
We found the analyzer was replacing the equivalent of thousands of manual checks.
Development quality was increased.
When adopting go-serum-analyzer in even a medium codebase...
By the numbers, we found:
When adopting go-serum-analyzer in even a medium codebase...
We found we were happy with the result.