Highly reliable Go code - Must and ValueOrDie
With Must
and ValueOrDie
, you can make error handling in Go a lot easier, clearer, and safer. Even better, this isn’t a new idea; it’s been around for a long time and is used in many places. You should use it too!
When writing Go code, a lot of methods can return an error value. Best practices dictate
that you not ignore this error (or any other return value), but instead do one
thing if the function succeeded and another if it returned a non-nil error. Because of this, Go code
has sometimes been accused of just being if err != nil { ... }
over and over again.
Sometimes, however, there is nothing that can be done when there is an error. Your program
can’t safely go on, and there’s no way of fixing the problem. Other times, the error is actually impossible, but the compiler
can’t prove that. For both cases, people have repeatedly come up with the same solution, and have even named it the same thing: Must()
.
The first instance I ever saw was MustCompile
in the regexp library. The regexp
compiler returns a compiled regular expression, and an error in case
compilation fails. But sometimes you are compiling a static string! In that
case, if compilation ever succeeds it will always succeed, and if it fails your
code is wrong and the program should crash.
Must
and its sibling ValueOrDie
The regexp library authors made regexp.MustCompile()
for crashing when a regexp failed to compile. There are also other MustX()
functions sprinkled around the standard library and in other peoples’ code. The name was evocative, and ever since, people have also proposed making a generic Must()
function and put their own Must()
functions as helpers. There’s lots of versions with slight variations, but the simplest one is easiest to understand:
func Must(err error) {
if err != nil {
panic(err)
}
}
Thanks to Go generics, we can add its twin
func ValueOrDie[T any](val T, err error) T {
if err != nil {
panic(err)
}
return val
}
With these two functions, setting up your systems in main()
is a lot
easier! Do you need to successfully open a file?
file = ValueOrDie(os.Open(filename))
does the job! Do you need to open a server on a port for serving a service?
listener := ValueOrDie(net.Listen("tcp", address))
works great! Do you need a string to unmarshal as json successfully?
Must(json.Unmarshal(scanner.Bytes(), &received))
to the rescue! In each of these cases, the error is fatal, and the program
should crash. The Must()
and ValueOrDie()
functions make this easy.
Functions as folklore
These functions are so useful that they keep getting written as an internal tool
in libraries. Most recently I found a Must()
in the uuid
library. It only works on UUIDs,
but it’s defined exactly like
ValueOrDie
and used in many important and popular library functions (e.g.
New()
).
Because this pair of little functions is repeatedly and similarly defined in many places, I would
encourage you to put them in a standard place in your own projects and use them liberally. They’ve passed the test of time! They also
really help when targeting high code coverage, because useless error code paths are
covered by the testing of Must()
rather than your tests being required to
invoke the error condition. You know for sure that if your program got to a
particular line, then the previous commands must have succeeded.
When I’m feeling whimsical, I think of these functions as the anthropic principle applied to code. They mean that your program ran successfully at the end because it got to the end, which means it ran successfully! Whimsy aside, this also means that your program is abiding by crash-on-error principles in some important parts, and crash-on-error programs have a history of being better in many contexts because they don’t try and stumble forward when things are broken; they immediately crash. The Pragmatic Programmer agrees! Tip 38 is “Crash Early. A dead program normally does a lot less damage than a crippled one.”
I highly recommend using a linter to make sure you never ignore a returned
error, and then that you use Must
and ValueOrDie
throughout for errors that
are impossible or unrecoverable. You save yourself the trouble of testing
those error conditions, and people reading your code don’t have to worry that
something might not have worked. It also follows the next Pragmatic Programmer
tip! Tip 39: “Use Assertions to Prevent the Impossible. If it can’t happen, use assertions to ensure that it won’t. Assertions validate your assumptions. Use them to protect your code from an uncertain world.”
When advice from 20 years ago is being actively used throughout many modern codebases and being constantly re-invented based on need, then I think they were really on to something. If you want your code to be reliable, make sure errors don’t happen. The only way to be sure they don’t is to either handle the errors (which makes them not erroneous, but instead an understood condition) or to crash (which ensures that your program will not erroneously try and make progress). Either way, you need to check the success condition, so turn on that linter and get to it!
Implications for the larger system
The only real requirement after this is a system that notices the crashes and “does the right thing”. In all cases, the number of crashes should be tracked, but sometimes the right thing is to allow the crash to take a larger system out of production, and other times the right thing to do is to restart the crashed program (“Have you tried turning it off and on again?” at scale).
Either way, you can put your system on a better-designed and better-tested path
by liberally using Must(...)
and ValueOrDie(...)
. These functions help assert that neither unhandleable errors nor inconceivable conditions are occurring.
Implementing Must
and ValueOrDie
By this point, I think we can agree that these functions are “folklore”. Implemented in many places, often with tiny local variations, but all basically the same. When I wrote open-source Go
code, I put these functions in a library called
rtx
(“run-time
extensions”). However, I now think that version is a bit overwrought. It tries too hard to be pretty and clever with its output.
The one I currently use in my projects is:
package rtx
import (
"os"
"log/slog"
"runtime/debug"
)
// We allow injection for testing.
var osExit = os.Exit
func die(err error) {
debug.PrintStack()
slog.Error("fatal error", "error", err)
osExit(1)
}
func Must(err error) {
if err != nil {
die(err)
}
}
func ValueOrDie[T any](obj T, err error) T {
if err != nil {
die(err)
}
return obj
}
I prefer this version for two reasons:
- It calls
os.Exit(1)
, which is unrecoverable, rather than callingpanic()
, which is recoverable. These functions are for unrecoverable situations. By using an unrecoverable function we remove the temptation to try and use this for fancy flow control. - It prints the stack trace and a simple
slog
message. I’m a big fan of structured logging and this does that, but it doesn’t get too precious about it by somehow trying to convert a stack trace into a structured log. Also, because we exit instead of panicking, it’s nice to print the stack trace because we don’t get it by default.
You might have slightly different preferences and that’s fine! Each teller tells their own version of a folklore story, and that’s part of the charm.
When to use Must
and ValueOrDie
✅ Good Use Cases:
- Program initialization and setup
- Loading configuration files
- Opening required system resources
- Compiling static regular expressions
- Parsing known-valid static data
- Test setup where failure means the test itself is broken
- Situations where a returned error can not occur
- Situations where recovery is impossible or meaningless
❌ Avoid Using For:
- Network operations that can be retried
- Any runtime data that could legitimately fail
- Any time where handling the error is desirable
- Errors that don’t matter
- Situations where partial failure is acceptable
The key question to ask yourself: “If this fails, does it make sense to continue doing anything?” If not, Must
and ValueOrDie
are appropriate.
Use it!
I hereby place the code in this blog post in the public domain (CC:0). Please use it for whatever you want in good health, you can even claim it as your own! It’s been written and rewritten so often that at this point it’s folklore and folklore has no owner. Also, if you can figure out where and how to put a version of it somewhere in the Go stdlib, please do that, so people don’t have to keep rewriting it, and we can finally settle on the one true implementation ;)
Happy coding!
Contribute to the discussion by replying on BlueSky!