{
"$type": "site.standard.document",
"canonicalUrl": "https://rednafi.com/go/anemic-stack-traces/",
"description": "Learn how to build custom error types in Go to create stack traces without runtime overhead, inspired by Rob Pike's Upspin error handling.",
"path": "/go/anemic-stack-traces/",
"publishedAt": "2024-02-10T00:00:00.000Z",
"site": "at://did:plc:fgtm2c26vfcj74rfmeggbyqj/site.standard.publication/3mnl6f7ob462z",
"tags": [
"Go",
"Error Handling",
"Logging"
],
"textContent": "While I like Go's approach of treating errors as values as much as the next person, it\ninevitably leads to a situation where there isn't a one-size-fits-all strategy for error\nhandling like in Python or JavaScript.\n\nThe usual way of dealing with errors entails returning error values from the bottom of the\ncall chain and then handling them at the top. But it's not universal since there are cases\nwhere you might want to handle errors as early as possible and fail catastrophically. Yet,\nit's common enough that we can use it as the base of our conversation.\n\nThis simple but verbose error handling works okay and makes us painfully aware of all the\npossible error paths. Yet, the model doesn't hold up as your program grows in scope and\ncomplexity, forcing you to devise custom patterns to add context and build thin stack\ntraces. There's no avoiding that.\n\nBut the good thing is that building an emaciated stack trace is fairly straightforward, and\nsome of the patterns are quite portable. After reading [Rob Pike's Upspin error handling\nblog], I had some ideas on creating custom errors to emulate stack traces. I ended up\nspending a few hours this morning experimenting with some of the ideas in a more limited\nscope.\n\nLet's say we're building a file-copy service that will accept a src and dst path and\ncopy the contents from source to destination.\n\nThis typical error handling pattern involves returning error values from lower-level\nfunctions and addressing them in top-level ones. Here, the main function manages the\nerror:\n\nRunning this function gives us the following output:\n\nThis is usually enough if you're building a CLI or a small program. Also, squinting at the\nerror message gives us a hint that among the 4 error-return paths, the copyFile function\nbailed at the first one when it couldn't find the source file.\n\nA proper way to handle this in larger applications is to wrap the errors and provide them\nwith your own context. Then, in the top-level function, you can unwrap the error message or\njust log it verbatim as before. So, copyFile can be rewritten as follows:\n\nNotice how we're adding extra context to the error values with the %w verb in the\nfmt.Errorf function.\n\nIf you keep the previous main function unchanged and run it, you'll get the following\noutput:\n\nThis time, since you know where you added the context, you also know which error-path the\ncopyFile function returned from. However, even in this case, the main function just\nrelays whatever comes out of copyFile and logs the error message.\n\nHow would you make the error message prettier without losing context? Also, how would you\nattach file names and line numbers to make debugging easier?\n\nThe debugging part isn't an issue in languages that support stack traces, this is usually\ntaken care of automatically. Now, whether that's a good thing or a bad thing is a discussion\nfor another day.\n\nWe can define a custom error struct to represent a generic error in the package that houses\ncopyFile.\n\nInside the Error struct, Op represents the name of the function that the error\noriginates from, Path is the file path, LineNo and FileName denote the precise\nlocation of the error, Err is the original error we're wrapping, and finally the debug\nboolean is be used to control the verbosity of error messages.\n\nThen the Error() method on the struct builds either a rudimentary stack trace or a\nprettier error message depending on the value of the Debug flag. The Error struct can be\nconstructed with the following constructor function:\n\nThis uses the runtime package to add the location data of the caller. It'll be called in\nthe copyFile function as follows:\n\nYou can turn on the Debug flag to print the stack trace in the main function:\n\nThe output will be:\n\nToggling Debug to false and running the snippet will return:\n\nYou can add even more context to this error in different calling locations like this:\n\nIt'll be pretty-printed like this when Debug is false:\n\nNow depending on your needs, you can customize the Error struct and NewError constructor\nto enable more elaborate error tracing.\n\nHowever, this isn't a proper stack in the sense that it only unwinds errors one level deep.\nBut it can be extended to recursively build the full error trace if needed. The [Upspin\nerror package] demonstrates a few techniques on how to do so. But for this particular case,\nanything more than a level deep stack is borderline overkill.\n\nHere's the [complete example on GitHub].\n\nFin!\n\n\n\n\n[rob pike's upspin error handling blog]:\n https://commandcenter.blogspot.com/2017/12/error-handling-in-upspin.html\n\n[upspin error package]:\n https://github.com/upspin/upspin/blob/master/errors/errors.go\n\n[complete example on github]:\n https://gist.github.com/rednafi/d090a16ba6ddd19c7fe8bdaae746205c",
"title": "Anemic stack traces in Go"
}