Simplicity in Go offers the “errors are values” concept, which makes someone who is only used to try ... catch
slightly nervous. Here are some tips on Golang error handling.
1. Capture the error before the function exits
We can use defer
and the named return value to capture the error before the function exits. Some use cases are:
- Log whatever error returned
- Overwrite whatever error returned
- Set default error
func function1() (err error) {
defer func() {
log.Println(err.Error()) // NOTE: log whatever error returned
}()
return errors.New("some-error")
}
func function2() (err error) {
defer func() {
err = errors.New("new-error") // NOTE: overwrite whatever error returned
}()
return errors.New("some-error")
}
func function3(cond bool) (err error) {
defer func(){
if err == nil {
err = errors.New("default-error") // NOTE: set default error
}
}()
if cond {
return errors.New("some-error")
}
return nil
}
2. Wrap the error
We can wrap the error into a new error with fmt.Errorf()
and %w
format specifier
err1 := fmt.Errorf("wrapped: %w", errors.New("original-error")) // NOTE: wrap the error
fmt.Println(err1.Error())
og := errors.Unwrap(err1) // NOTE: unwrap the error
fmt.Println(og.Error())
// Output:
// wrapped: original-error
// original-error
The wrapped error is the same as the original error. We can use errors.Is()
to check.
err := errors.New("original-error")
wrapped := fmt.Errorf("wrapped: %w", err)
wrappedAgain := fmt.Errorf("wrapped-again: %w", wrapped)
fmt.Println(wrapped == err) // FALSE
fmt.Println(errors.Unwrap(wrapped) == err) // TRUE
fmt.Println(errors.Is(wrapped, err)) // TRUE
fmt.Println(errors.Is(err, wrapped)) // FALSE
fmt.Println(errors.Is(wrappedAgain, err)) // TRUE
3. Custom Error Type
We can create our custom error type by implement error
interface
type CustomError struct {
Message string
}
var _ error = (*CustomError)(nil) // NOTE: Make sure CustomError implement error interface
func (c *CustomError) Error() string {
return c.Message
}
Instead of type assertion, we can use errors.As()
to cast the error type.
var err error = &CustomError{"some-error-occurred"}
// use type assertion
if customErr, ok := err.(*CustomError); ok {
fmt.Println(customErr.Error())
}
// use `errors.As()`
var customErr2 *CustomError
if errors.As(err, &customErr2) {
fmt.Println(customErr2.Error())
}
// use `errors.As()` for wrapped error
var wrapped error = fmt.Errorf("wrapped: %w", err)
var customErr3 *CustomError
if errors.As(wrapped, &customErr3) {
fmt.Println(customErr2.Error())
}
4. Handle multiple errors
We can handle multiple errors with errors
standard package or multierr
library from Uber.
err1 := errors.Join(errors.New("err-1"), errors.New("err-2")) // NOTE: can't return the underlying list of error
fmt.Println("Joining errors with `errors` package")
fmt.Println(err1.Error())
// Output: error message use '\n' delimiter
// err-1
// err-2
err2 := multierr.Combine(errors.New("err-3"), errors.New("err-4"))
fmt.Println("Combining errors with `multierr` package")
fmt.Println(err2.Error())
// Output: error message use ';' delimiter
// err-3; err-4
errs := multierr.Errors(err2) // NOTE: return underlying list of error
fmt.Println(len(errs))
We can also append the captured error (Tips#1) with the deferred operation (i.e. close operation).
// NOTE: Check more example at https://pkg.go.dev/go.uber.org/multierr#hdr-Deferred_Functions
func sendRequest(req Request) (err error) {
conn, err := openConnection()
if err != nil {
return err
}
defer multierr.AppendInvoke(&err, multierr.Close(conn)) // NOTE: Actually it is safe to ignore the error from close() operations at most of the time
// ...
}
Share your thoughts and other tips on handling errors in Go!