Go
5 Levels of Go Error Handling

Understanding if err != nil

Error handling in Go

Go has a built-in error handling mechanism that allows you to handle errors in a more structured and readable way. This mechanism is based on the concept of error values, which are represented by the error interface.

type error interface {
    Error() string
}
  • method called Error() that returns a string representation of the error
  • any interface type in Go can be satisfied by nil (as a valid value)

Creating and returning own errors

func divide(a, b int) (int, error) {
    if b == 0 {
        return 0, errors.New("divide: cannot divide by zero")
        // fmt.Errorf() works as well -> error wrapping with %w, more info below
    }
    return a / b, nil
}
 
func TestDivideByZero(t *testing.T) {
    _, err := divide(10, 0)
    if err == nil {
        t.fatal("expected division error, got nil")
    }
    t.log(err)
}

Checking errors

var ErrUnsupportedMode = errors.New("unsupported mode")
 
func operation(mode int) (string, error){
    if mode < 0 {
        // return "", errors.New("invalid mode") 
        // checking for errors like this is brittle and error-prone esp when testing for the error, instead create an accessible and shared error type very easily checkable errors
        return "", ErrUnsupportedMode
    }
    return fmt.Sprintf("running operation with mode %d", mode), nil
}
 
func TestOperationUnsupported(t *testing.T) {
    _, err := operation(-1)
    if err == nil {
        t.Fatal("expected error, got nil")
    }
    if !errors.Is(err, ErrUnsupportedMode) {
        t.Fatalf("expected error to be %v, got %v", ErrUnsupportedMode, err)
    }
    t.Log(err)
}

Custom error types

  • one issue with the already improved approach above is that we are restricted to the same error message for all errors, which is not very helpful
  • we can create our own error types with the Error interface
 
type UnsupportedModeError struct {
    mode int
}
 
func (e *UnsupportedModeError) Error() string {
    return fmt.Sprintf("operation: unsupported mode %d", e.mode)
}
 
func operation(mode int) (string, error){
    if mode < 0 {
        return "", &ErrUnsupportedModeError{mode: mode} 
    }
    return fmt.Sprintf("running operation with mode %d", mode), nil
}
 
func TestOperationUnsupported(t *testing.T) {
    _, err := operation(-1)
    if err == nil {
        t.Fatal("expected error, got nil")
    }
 
    var unsupportedErr *UnsupportedModeError // quick variable that is a value of type UnsupportedModeError
 
    // error we wanna check for and the address to a spot to cast the error to
    if !errors.As(err, &unsupportedErr) {
        t.Fatalf("expected error to be %v, got %v", ErrUnsupportedMode, err)
    }
    t.Log(err)
}

Errors.As vs Errors.Is

Errors.As takes in err we are checking for and a target variable, and if the error is of the type of the target variable, it will be assigned to the target variable

Errors.Is takes in err and an error interface, and if the error is equal to the target

Wrapped errors

  • when you have an error and want to "put something around it"
func TestUnwrap(t *testing.T) {
    err := fmt.Errorf("error: %w", errors.New("wrapped error"))
    t.log(err) // error: wrapped error
 
    underlyingErr := errors.Unwrap(err)
    t.log(underlyingErr) // wrapped error
}

Errors as value === we can use nay programming patten to deal with errors.

At each level where error is wrapped, you may add a message. A cause, or maybe just a function name or other identification of level of abstraction.

Example of handling nested calls and errors on a low level

func readFromFile() (string, error) {
    data, err := os.ReadFile("wrong file name")
    if err != nil {
        return "", errors.Wrap(err, "readFromFile")
    }
    return string(data), nil
}
 
func readConfig() (string, error) {
    data, err := readFromFile()
    if err != nil {
        return "", errors.Wrap(err, "readConfig")
    }
    // ...
    return data, nil
}
 
func main() {
    conf, err := readConfig()
    if err != nil {
        log.Printf("Cannot read: %v", err)
    }
}
 
// output
// 2022/03/16 00:37:54 Cannot read: readConfig: readFromFile: open wrong file name: no such file or directory
 
// Retrieving root cause
errors.Cause(err)
 
// Built-in error wrapping - note that this form does not preserve stack traces
err = fmt.Errorf("read file: %w", err)
 
// Stack traces (errors wrapped with errors.Wrap() function preserves the call stack)
log.printf("Cannot read: %+v", err)
 
// output
/*
2022/07/22 18:51:54 Cannot read: open wrong file name: no such file or directory readFromFile
awesomeProject/learnWrapping.readFromFile
        /Code/learn/go/awesomeProject/learnWrapping/wrapping.go:13
awesomeProject/learnWrapping.readConfig
        /Code/learn/go/awesomeProject/learnWrapping/wrapping.go:19
awesomeProject/learnWrapping.Main
        /Code/learn/go/awesomeProject/learnWrapping/wrapping.go:28
main.main
        /Users/tomek/Code/learn/go/awesomeProject/main.go:6
*/

References