Go error handling

Márk Sági-Kazár



Errors are values

package foo

import "errors"

func Foo() error {
    errors.New("something went wrong")
package bar

import "foo"

func Bar() error {
    return foo.Foo()

What happened?

package main

import (


func main() error {

// Output: something went wrong (?)

Standard library solutions

Error prefixing

package foo

import "errors"

func Foo() error {
    errors.New("foo: something went wrong")

Sentinel errors

package io

// ...

// EOF is the error returned by Read
// when no more input is available.
var EOF = errors.New("EOF")

Not enough!!!

  • Stack trace
  • Propagation
  • Implicit interface compatibility


  • Stack trace
  • Additional message
  • Root cause


  • Extends github.com/pkg/errors
  • Error handling tools


Go 1.12: Modules

Emperror became a huge dependency

Solution: Split up the module into smaller ones

New Emperror library

import "emperror.dev/handler/logrus"
// VS
import logrus "github.com/emperror/handler-logrus"

Go 1.13: Errors

  • Unwrap
  • As
  • Is


type myError struct { err error }

func (e myError) Error() string { return e.err.Error() }
func (e myError) Unwrap() error { return e.err }
func (e myError) Cause() error { return e.err }

err1 := errors.New("error")
err2 := &myError{err1}
err3 := &myError{err2}

err := errors.Unwrap(err3) // == err2
// BUT
err := pkgErrors.Cause(err3) // == err1 (!!!)


err := pkgErrors.New("error")

var stackTracer interface{ StackTrace() pkgErrors.StackTrace }
if errors.As(err, &stackTracer) {
    st := stackTracer.StackTrace()

“Assert errors for behaviour, not type” - Dave Cheney


err := someFileReading()

if errors.Is(err, io.EOF) {
    fmt.Println("help, IO error occurred")

Go 1.13: Errors

Partial incompatibility with github.com/pkg/errors

Only from Go 1.13

No stack trace

Solution: New error library


Drop-in replacement for errors and github.com/pkg/errors

Merged parts of Emperror into the new library

Tested with replaced library test suites

What changed?

New packages/modules

Old New
github.com/goph/emperror emperror.dev/emperror
github.com/goph/emperror/handler/*handler emperror.dev/handler/*
(no previous module) emperror.dev/errors


Old New
emperror.Wrap errors.WrapIf
emperror.Wrapf emperror.WrapIff
emperror.With errors.WithDetails
emperror.WrapWith errors.WrapIfWithDetails
emperror.Context errors.GetDetails
emperror.NewMultiErrorBuilder errors.Combine
emperror.ForEachCause errors.UnwrapEach
emperror.HandlerWith emperror.WithDetails
emperror.HandlerWithPrefix (no replacement yet)

New functions

  • NewWithDetails
  • NewPlain
  • WithStackDepth / WithStackDepthIf

Documentation and examples


New errors

// annotated with stack trace
err := errors.New("something went wrong")
err := errors.Errorf("something went %s", "wrong")

// annotated with stack trace and details
err := errors.NewWithDetails(
    "something went wrong",
    "key", "value",

Custom errors

type TodoNotFoundError struct{
    TodoID int

func (TodoNotFoundError) Error() string {
    return "todo not found"

func (e TodoNotFoundError) Details() []interface{} {
    return []interface{}{"todo_id", e.TodoID}

// ...
err := errors.WithStack(TodoNotFoundError{TodoID: 1})

Custom errors

var e TodoNotFoundError
if errors.As(err, &e) {
    fmt.Printf("todo id: %d", e.TodoID)

// OR

e := TodoNotFoundError{TodoID: 1}
if errors.Is(err, e) {
    fmt.Printf("todo id: %d", e.TodoID)

Custom errors

var e interface{ NotFound() bool }
if errors.As(err, &e) {
    fmt.Println("todo not found")

Sentinel errors

// create an error without stack trace
var ErrNotFound = errors.NewPlain("not found")

// ...

// annotate the error with stack trace
err := errors.WithStack(ErrNotFound)

// ...

// check for error equality
if errors.Is(err, ErrNotFound) {

Error wrapping

var err error

// *If functions only annotate err with a stack trace
// if there isn't one already in err's chain
err = errors.WrapIf(err, "error passed through here")

// add key-value pairs to err
// access them with errors.GetDetails
err = errors.WithDetails(err, "key", "value")

Multi errors

var errs []error

for !exitCondition {
    err := funcThatErrors()
    errs = append(errs, err)

// combines several errors into a single one
// nil values are removed
err := errors.Combine(errs...)

Further reading




Go 2 proposals