summaryrefslogtreecommitdiff
path: root/src/_posts/2021-03-20-a-simple-rule-for-better-errors.md
diff options
context:
space:
mode:
Diffstat (limited to 'src/_posts/2021-03-20-a-simple-rule-for-better-errors.md')
-rw-r--r--src/_posts/2021-03-20-a-simple-rule-for-better-errors.md227
1 files changed, 0 insertions, 227 deletions
diff --git a/src/_posts/2021-03-20-a-simple-rule-for-better-errors.md b/src/_posts/2021-03-20-a-simple-rule-for-better-errors.md
deleted file mode 100644
index 30139fb..0000000
--- a/src/_posts/2021-03-20-a-simple-rule-for-better-errors.md
+++ /dev/null
@@ -1,227 +0,0 @@
----
-title: >-
- A Simple Rule for Better Errors
-description: >-
- ...and some examples of the rule in action.
-tags: tech
----
-
-This post will describe a simple rule for writing error messages that I've
-been using for some time and have found to be worthwhile. Using this rule I can
-be sure that my errors are propagated upwards with everything needed to debug
-problems, while not containing tons of extraneous or duplicate information.
-
-This rule is not specific to any particular language, pattern of error
-propagation (e.g. exceptions, signals, simple strings), or method of embedding
-information in errors (e.g. key/value pairs, formatted strings).
-
-I do not claim to have invented this system, I'm just describing it.
-
-## The Rule
-
-Without more ado, here's the rule:
-
-> A function sending back an error should not include information the caller
-> could already know.
-
-Pretty simple, really, but the best rules are. Keeping to this rule will result
-in error messages which, once propagated up to their final destination (usually
-some kind of logger), will contain only the information relevant to the error
-itself, with minimal duplication.
-
-The reason this rule works in tandem with good encapsulation of function
-behavior. The caller of a function knows only the inputs to the function and, in
-general terms, what the function is going to do with those inputs. If the
-returned error only includes information outside of those two things then the
-caller knows everything it needs to know about the error, and can continue on to
-propagate that error up the stack (with more information tacked on if necessary)
-or handle it in some other way.
-
-## Examples
-
-(For examples I'll use Go, but as previously mentioned this rule will be useful
-in any other language as well.)
-
-Let's go through a few examples, to show the various ways that this rule can
-manifest in actual code.
-
-**Example 1: Nothing to add**
-
-In this example we have a function which merely wraps a call to `io.Copy` for
-two files:
-
-```go
-func copyFile(dst, src *os.File) error {
- _, err := io.Copy(dst, src)
- return err
-}
-```
-
-In this example there's no need to modify the error from `io.Copy` before
-returning it to the caller. What would we even add? The caller already knows
-which files were involved in the error, and that the error was encountered
-during some kind of copy operation (since that's what the function says it
-does), so there's nothing more to say about it.
-
-**Example 2: Annotating which step an error occurs at**
-
-In this example we will open a file, read its contents, and return them as a
-string:
-
-```go
-func readFile(path string) (string, error) {
- f, err := os.Open(path)
- if err != nil {
- return "", fmt.Errorf("opening file: %w", err)
- }
- defer f.Close()
-
- contents, err := io.ReadAll(f)
- if err != nil {
- return "", fmt.Errorf("reading contents: %w", err)
- }
-
- return string(contents), nil
-}
-```
-
-In this example there are two different steps which could result in an error:
-opening the file and reading its contents. If an error is returned then our
-imaginary caller doesn't know which step the error occurred at. Using our rule
-we can infer that it would be good to annotate at _which_ step the error is
-from, so the caller is able to have a fuller picture of what went wrong.
-
-Note that each annotation does _not_ include the file path which was passed into
-the function. The caller already knows this path, so an error being returned
-back which reiterates the path is unnecessary.
-
-**Example 3: Annotating which argument was involved**
-
-In this example we will read two files using our function from example 2, and
-return the concatenation of their contents as a string.
-
-```go
-func concatFiles(pathA, pathB string) (string, error) {
- contentsA, err := readFile(pathA)
- if err != nil {
- return "", fmt.Errorf("reading contents of %q: %w", pathA, err)
- }
-
- contentsB, err := readFile(pathB)
- if err != nil {
- return "", fmt.Errorf("reading contents of %q: %w", pathB, err)
- }
-
- return contentsA + contentsB, nil
-}
-```
-
-Like in example 2 we annotate each error, but instead of annotating the action
-we annotate which file path was involved in each error. This is because if we
-simply annotated with the string `reading contents` like before it wouldn't be
-clear to the caller _which_ file's contents couldn't be read. Therefore we
-include which path the error is relevant to.
-
-**Example 4: Layering**
-
-In this example we will show how using this rule habitually results in easy to
-read errors which contain all relevant information surrounding the error. Our
-example reads one file, the "full" file, using our `readFile` function from
-example 2. It then reads the concatenation of two files, the "split" files,
-using our `concatFiles` function from example 3. It finally determines if the
-two strings are equal:
-
-```go
-func verifySplits(fullFilePath, splitFilePathA, splitFilePathB string) error {
- fullContents, err := readFile(fullFilePath)
- if err != nil {
- return fmt.Errorf("reading contents of full file: %w", err)
- }
-
- splitContents, err := concatFiles(splitFilePathA, splitFilePathB)
- if err != nil {
- return fmt.Errorf("reading concatenation of split files: %w", err)
- }
-
- if fullContents != splitContents {
- return errors.New("full file's contents do not match the split files' contents")
- }
-
- return nil
-}
-```
-
-As previously, we don't annotate the file paths for the different possible
-errors, but instead say _which_ files were involved. The caller already knows
-the paths, there's no need to reiterate them if there's another way of referring
-to them.
-
-Let's see what our errors actually look like! We run our new function using the
-following:
-
-```go
- err := verifySplits("full.txt", "splitA.txt", "splitB.txt")
- fmt.Println(err)
-```
-
-Let's say `full.txt` doesn't exist, we'll get the following error:
-
-```
-reading contents of full file: opening file: open full.txt: no such file or directory
-```
-
-The error is simple, and gives you everything you need to understand what went
-wrong: while attempting to read the full file, during the opening of that file,
-our code found that there was no such file. In fact, the error returned by
-`os.Open` contains the name of the file, which goes against our rule, but it's
-the standard library so what can ya do?
-
-Now, let's say that `splitA.txt` doesn't exist, then we'll get this error:
-
-```
-reading concatenation of split files: reading contents of "splitA.txt": opening file: open splitA.txt: no such file or directory
-```
-
-Now we did include the file path here, and so the standard library's failure to
-follow our rule is causing us some repitition. But overall, within the parts of
-the error we have control over, the error is concise and gives you everything
-you need to know what happened.
-
-## Exceptions
-
-As with all rules, there are certainly exceptions. The primary one I've found is
-that certain helper functions can benefit from bending this rule a bit. For
-example, if there is a helper function which is called to verify some kind of
-user input in many places, it can be helpful to include that input value within
-the error returned from the helper function:
-
-```go
-func verifyInput(str string) error {
- if err := check(str); err != nil {
- return fmt.Errorf("input %q was bad: %w", str, err)
- }
- return nil
-}
-```
-
-`str` is known to the caller so, according to our rule, we don't need to include
-it in the error. But if you're going to end up wrapping the error returned from
-`verifyInput` with `str` at every call site anyway it can be convenient to save
-some energy and break the rule. It's a trade-off, convenience in exchange for
-consistency.
-
-Another exception might be made with regards to stack traces.
-
-In the set of examples given above I tended to annotate each error being
-returned with a description of where in the function the error was being
-returned from. If your language automatically includes some kind of stack trace
-with every error, and if you find that you are generally able to reconcile that
-stack trace with actual code, then it may be that annotating each error site is
-unnecessary, except when annotating actual runtime values (e.g. an input
-string).
-
-As in all things with programming, there are no hard rules; everything is up to
-interpretation and the specific use-case being worked on. That said, I hope what
-I've laid out here will prove generally useful to you, in whatever way you might
-try to use it.
-