Overwrite Returned Error and Other Tips in Go

Iman Tung
3 min readJun 27, 2024

--

Image Source

Simplicity in Go offers the “errors are values” concept, which makes someone who is only used to try ... catchslightly 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!

--

--

Iman Tung

Technology to write, life to grateful. Overthinking is good, only if it has the output. Fundamental is the main concern.