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
*/