The Art of Error Handling in Go

      ☕ 5 min read
🏷️
  • #Golang
  • Error handling in Go can be confusing for beginners and for those accustomed to other programming languages. In this article, we will distinguish between errors (error) and exceptions (panic), discuss what constitutes a “good” error (one that is easy to check and debug), and present a common method for improving error handling through the use of fmt.Errorf().

    By the way, if you’re new to Go and come from a different programming language background, I recommend reading my previous post, “The Second Language Guide to Golang”.

    In this context, “users” refers to developers writing code (that’s you!).

    Distinguishing Between Exceptions (panic) and Errors (error)

    Go categorizes unexpected program states into two types: panic (exceptions) and error (errors).

    Similar to throw in Java/JS or raise in Python, panic halts program execution and enters the exception handling logic. Exceptions can be caught and handled using defer recover at any level of the call stack. Uncaught panics cause the program to exit abnormally after printing the stack trace. panic is rarely used in user code and is generally reserved for language-level issues such as division by zero, out of memory errors, or type assertion failures. These are uncommon and usually indicate a flaw in the user’s implementation, so they’re not typically the focus of defensive programming.

    error, on the other hand, is much more mundane. Any value that satisfies the error interface can be considered a valid error:

    1
    2
    3
    4
    
    type error interface {
        // Returns a text description of the error
        Error() string
    }
    

    error is just a value, treated no differently by the runtime; ignoring it won’t cause the program to exit with a stack trace.

    Errors are prevalent in user code, representing issues such as connection timeouts, JSON parsing failures, or file not found errors. These are common, often directly related to the business logic, and typically require defensive programming.

    In summary:

    • panic: An exception not focused on defense, with no specific throw location.
    • error: An error that requires careful consideration and handling by the user, with a fixed and obvious throw location. Whenever a function’s return value tuple includes an error type, the user needs to consider how to handle it.

    In other words, the fact that error is a value in Go encourages users to consider and handle each error, leading to robust and resilient programs.

    We Prefer “Good” Errors

    Yes, errors can be good or bad.

    For example, suppose you have a weather API service that checks Redis for cached responses before querying an external service. If the call fails, the error message might simply say context deadline exceeded, indicating a timeout somewhere. But where? Was it the cache or the external API?

    Good errors provide clear causes and context from the call stack, making them easy to diagnose without guesswork. Imagine if the error message instead read:

    reading cache: redis GET: context deadline exceeded
    

    Or included a stack trace:

    context deadline exceeded
    goroutine 1 [running]:
    main.Example(0x19010001)
               /Users/hello/main.go
               temp/main.go:8 +0x64
    main.main()
               /Users/bill/main.go
               temp/main.go:4 +0x32
    

    Your debugging effort would be much easier.

    I prefer the first method; manually added textual error contexts are more readable, contain higher information density, and can be understood even by those unfamiliar with the code.

    Providing Context to Errors

    Introduced in Go 1.13 (September 2019), fmt.Errorf() allows for adding context to errors, and the errors standard library added Is(), As(), and Unwrap() for easier error discrimination and comparison (a topic for another day).

    This feature was decided upon after much discussion; the Go maintainers are generally cautious about introducing new features.

    Before Go 1.13, the community experimented with various approaches, with the third-party library pkg/errors being a notable example. It explored both adding context and stack traces to errors.

    The usage of fmt.Errorf() looks something like this, recalling the “good error” we aimed for:

    reading cache: redis GET: context deadline exceeded
    

    This error unfolds like a story, progressing layer by layer from left to right. Each layer of fmt.Errorf() narrates what it attempts to do, followed by a colon (:) that separates it from the next layer, with the %w verb indicating an error to wrap. In the pseudo-code below, where FindWeather() calls ReadCache(), consider the error received by the caller of FindWeather() when ReadCache() encounters an error:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    
    func FindWeather(city string) (weather string, err error) {
    	weather, err = ReadCache(city)
    	if err != nil {
    		err = fmt.Errorf("reading cache: %w", err)
    		return
    	}
    	
    	if weather != "" {
    		// cache hit
    		return 
    	}
    	
    	// cache missed, query for data source and update cache
    	// ...
    }
    
    func ReadCache(city string) (weather string, err error) {
    	cacheKey := "city-" + city
    	weather, err = cache.Get(cacheKey)
    	if err == redis.Nil {
    		// cache missed
    		err = nil
    		return 
    	} else if err != nil {
    		err = fmt.Errorf("redis Get: %w", err)
    		return
    	}
    	
    	return 
    }
    

    Indeed, repeating if err != nil and fmt.Errorf() does require a bit of typing (a trade-off), but consider configuring your favorite IDE’s autocomplete feature to save some keystrokes.

    Static analysis tools for Go can help identify errors in the code that you forgot to handle (sometimes critically so) and can flag improper error context formatting (e.g., %d, %f, %v, %w, %x). I usually employ a collection of these tools in the form of golangci-lint.

    Other Ways to Add Context to Errors

    Although the official introduction of textual error context into the standard library signals a commitment to this approach, you might still find other methods for adding context to errors useful. e.g. Adding stack traces to errors with pkg/errors.

    References

    Share on

    nanmu42
    WRITTEN BY
    nanmu42
    To build beautiful things beautifully.

    What's on this Page