Let's talk about programmable errors and how you can design your own architecture that allows you to legibly master your system failures.
Summary
Errors are i/o:
Sometimes you need to read an error
Sometimes you need to write an error
How can this be improved?
Don't just check errors => Handle them gracefully
Stack traces are for disasters: Decorate errors for better tracing
How to improve error tracing?
Categorize errors by severity
Categorize errors by type
Add application specific data
You can query all of the above
Learning concepts vs. Learning syntax
Assuming Go isn't your first programming language, there are two ways to approach diving into a new language, Marwan explains. When learning syntax, one can ask:
How do I parse a JSON string in Go?
If on other hand, we are learning the concept, the question is:
What is data serialization?
Another example:
Syntax: How do I import a library in Go?
Concept: What are dependencies?
And with regards to errors:
Syntax: How do I catch an error in Go?
Concept: What is error handling?
Let's focus on concepts and use the flexibility of Go to create better tooling for error handling and tracing.
Concept of Errors
Errors are values:
PRO: You get to define the importance of errors
CON: You get to define the importance of errors
We can furthermore think of errors as just i/o:
Sometimes you need to read an error
Sometimes you need to write an error
But when reading or writing an error, context matters in understanding what caused the error as well as how to address it. e.g.:
Is your program a CLI tool?
Is your program a library?
Is your program a long running system?
Who is consuming your program? And How?
Example: Make a sandwich
Let's pretend we are designing a service that fetches ingredients from different sources and then returns a slice of ingredients. Our code may look like:
Now the above code assumes there are no errors. So how do we handle errors? Well we can rewrite getIngredients to actually return an error when they are returned from an upstream module:
So what would we see now if we ran the above?
As we can see the default behavior leaves out a lot of information about the context of the error. "Don't just check errors, handle them gracefully" - Marwan
Let's now discuss how we can decorate errors in go:
Can use fmt to create a new error with more context:
Alternatively can import "github.com/pkg/errors" and use this module's wrapping ability:
Let's look at the getIngredients function now:
Now we have additional context in our error log and no longer need stacktrace:
Stacktraces are for disasters
They're hard to read
They're hard to parse
At best, they say where an error went wrong, and not why
What if we want to act on an error?
Since errors are values we can create specific ones and compare to take specific actions:
Now we can handle errors gracefully, trace the error back to the code and act upon an error. What more can we do?
Categorize errors by severity.
Categorize errors by type.
Add application specific data.
Query all of the above.
Let's take a look at how this relates to The New York Times. Like many companies, The New York Times has several services that talk to each other:
Not much different from making sandwiches:
One service that talks to a bunch of other services
Instead of panicking, we log and monitor
and in an http handler:
Now our errors are logged and we have some context:
However, we are still missing severity as well as types of errors so how can this be improved?