TL;DR

  • errors standard library package in golang 1.13 cannot be alternatives of pkg/errors and xerrors.
  • Use xerrors for error handling for new golang projects.
  • No need to switch from pkg/errors to xerrors.

Motivation

golang is a very simple language and that’s the core philosophy of its design. Along with the design, standard libraries are also simple and don’t do too many things. However, the features of error standard library is not enough for developments. It cannot traverse stack traces and developers are difficult to debug if we only use the library for error handling. To introduce stack traces, we use pkg/errors or xerrors.

From golang 1.13, the error standard library finally incorporated some features implemented in xerrors. So, let me summarize my conclusions whether we can migrate from pkg/errors to the errors standard library or not.

Prerequisite

  • The target golang version for this article is 1.13

Migration from pkg/errors to errors

Unfortunately, I don’t think we can migrate pkg/errors to the error standard library because the error package in golang 1.13 cannot provide stack traces. The following code tries to wrap error chains and output by fmt.Printf() with %+v.

package main

import (
	"errors"
	"fmt"
	"os"
)

func firstFunc() error {
	err := secondFunc()
	if err != nil {
		err = fmt.Errorf("Wrap in firstFunk: %w", err)
		return err
	}
	return nil
}

func secondFunc() error {
	err := thirdFunc()
	if err != nil {
		err = fmt.Errorf("Wrap in secondFunk: %w", err)
		return err
	}
	return nil
}

func thirdFunc() error {
	return errors.New("Error produced by errors.New() in thirdFunc")
}

func main() {
	err := firstFunc()
	if err != nil {
		fmt.Println(`### Outputs for fmt.Printf("[ERROR]: %+v\n", err)`)
		fmt.Printf("[ERROR]: %+v\n\n", err)

		fmt.Println(`### Outputs for fmt.Printf("[ERROR]: %+v\n", errors.Unwrap(err))`)
		fmt.Printf("[ERROR]: %+v\n", errors.Unwrap(err))
		os.Exit(1)
	}
	fmt.Println("Success")
	os.Exit(0)
}

You can run this code on Go Playground. The result of the above code is as follows. This is better than the behavior of the error packages before 1.13 though what we really want the errors package to incorporate is a stack tracing.

### Outputs for fmt.Printf("[ERROR]: %+v\n", err)
[ERROR]: Wrap in firstFunk: Wrap in secondFunk: Error produced by errors.New() in thirdFunc

### Outputs for fmt.Printf("[ERROR]: %+v\n", errors.Unwrap(err))
[ERROR]: Wrap in secondFunk: Error produced by errors.New() in thirdFunc

Stack traces with pkg/errors

If we run the same code with pkg/errors we can gain stack traces.

package main

import (
	"fmt"
	"os"

	"github.com/pkg/errors"
)

func firstFunc() error {
	err := secondFunc()
	if err != nil {
		err = errors.Wrap(err, "Wrap in firstFunk: ")
		return err
	}
	return nil
}

func secondFunc() error {
	err := thirdFunc()
	if err != nil {
		err = errors.Wrap(err, "Wrap in secondFunk: ")
		return err
	}
	return nil
}

func thirdFunc() error {
	return errors.New("Error produced by errors.New() in thirdFunc")
}

func main() {
	err := firstFunc()
	if err != nil {
		fmt.Println(`### Outputs for fmt.Printf("[ERROR]: %+v\n", err)`)
		fmt.Printf("[ERROR]: %+v\n\n", err)

		fmt.Println(`### Outputs for fmt.Printf("[ERROR]: %+v\n", errors.Cause(err))`)
		fmt.Printf("[ERROR]: %+v\n", errors.Cause(err))
		os.Exit(1)
	}
	fmt.Println("Success")
	os.Exit(0)
}

You can run the above code on Go Playground and get the result below. As you can see, stack traces are listed.

### Outputs for fmt.Printf("[ERROR]: %+v\n", err)
[ERROR]: Error produced by errors.New() in thirdFunc
main.thirdFunc
	/tmp/sandbox141164394/prog.go:29
main.secondFunc
	/tmp/sandbox141164394/prog.go:20
main.firstFunc
	/tmp/sandbox141164394/prog.go:11
main.main
	/tmp/sandbox141164394/prog.go:33
runtime.main
	/usr/local/go-faketime/src/runtime/proc.go:203
runtime.goexit
	/usr/local/go-faketime/src/runtime/asm_amd64.s:1373
Wrap in secondFunk: 
main.secondFunc
	/tmp/sandbox141164394/prog.go:22
main.firstFunc
	/tmp/sandbox141164394/prog.go:11
main.main
	/tmp/sandbox141164394/prog.go:33
runtime.main
	/usr/local/go-faketime/src/runtime/proc.go:203
runtime.goexit
	/usr/local/go-faketime/src/runtime/asm_amd64.s:1373
Wrap in firstFunk: 
main.firstFunc
	/tmp/sandbox141164394/prog.go:13
main.main
	/tmp/sandbox141164394/prog.go:33
runtime.main
	/usr/local/go-faketime/src/runtime/proc.go:203
runtime.goexit
	/usr/local/go-faketime/src/runtime/asm_amd64.s:1373

### Outputs for fmt.Printf("[ERROR]: %+v\n", errors.Cause(err))
[ERROR]: Error produced by errors.New() in thirdFunc
main.thirdFunc
	/tmp/sandbox141164394/prog.go:29
main.secondFunc
	/tmp/sandbox141164394/prog.go:20
main.firstFunc
	/tmp/sandbox141164394/prog.go:11
main.main
	/tmp/sandbox141164394/prog.go:33
runtime.main
	/usr/local/go-faketime/src/runtime/proc.go:203
runtime.goexit
	/usr/local/go-faketime/src/runtime/asm_amd64.s:1373

Stack traces with xerrors

This time, let’s use xerrors to gain stack traces.

package main

import (
	"fmt"
	"os"

	"golang.org/x/xerrors"
)

func firstFunc() error {
	err := secondFunc()
	if err != nil {
		err = xerrors.Errorf("Wrap in firstFunk: %w", err)
		return err
	}
	return nil
}

func secondFunc() error {
	err := thirdFunc()
	if err != nil {
		err = xerrors.Errorf("Wrap in secondFunk: %w", err)
		return err
	}
	return nil
}

func thirdFunc() error {
	return xerrors.New("Error produced by errors.New() in thirdFunc")
}

func main() {
	err := firstFunc()
	if err != nil {
		fmt.Println(`### Outputs for fmt.Printf("[ERROR]: %+v\n", err)`)
		fmt.Printf("[ERROR]: %+v\n\n", err)

		fmt.Println(`### Outputs for fmt.Printf("[ERROR]: %+v\n", xerrors.Unwrap(err))`)
		fmt.Printf("[ERROR]: %+v\n", xerrors.Unwrap(err))
		os.Exit(1)
	}
	fmt.Println("Success")
	os.Exit(0)
}

Again, you can execute the above code on Go Playground and get the result below. xerrors also provides stack traces.

### Outputs for fmt.Printf("[ERROR]: %+v\n", err)
[ERROR]: Wrap in firstFunk:
    main.firstFunc
        /tmp/sandbox583951390/prog.go:13
  - Wrap in secondFunk:
    main.secondFunc
        /tmp/sandbox583951390/prog.go:22
  - Error produced by errors.New() in thirdFunc:
    main.thirdFunc
        /tmp/sandbox583951390/prog.go:29

### Outputs for fmt.Printf("[ERROR]: %+v\n", xerrors.Unwrap(err))
[ERROR]: Wrap in secondFunk:
    main.secondFunc
        /tmp/sandbox583951390/prog.go:22
  - Error produced by errors.New() in thirdFunc:
    main.thirdFunc
        /tmp/sandbox583951390/prog.go:29

Diffs between pkg/errors, xerrors, and errors

stack traces

pkg/errors

### Outputs for fmt.Printf("[ERROR]: %+v\n", errors.Cause(err))
[ERROR]: Error produced by errors.New() in thirdFunc
main.thirdFunc
	/tmp/sandbox141164394/prog.go:29
main.secondFunc
	/tmp/sandbox141164394/prog.go:20
main.firstFunc
	/tmp/sandbox141164394/prog.go:11
main.main
	/tmp/sandbox141164394/prog.go:33
runtime.main
	/usr/local/go-faketime/src/runtime/proc.go:203
runtime.goexit
	/usr/local/go-faketime/src/runtime/asm_amd64.s:1373
  • I like this style of stack traces

xerrors

### Outputs for fmt.Printf("[ERROR]: %+v\n", xerrors.Unwrap(err))
[ERROR]: Wrap in secondFunk:
    main.secondFunc
        /tmp/sandbox583951390/prog.go:22
  - Error produced by errors.New() in thirdFunc:
    main.thirdFunc
        /tmp/sandbox583951390/prog.go:2
  • I don’t like this style of stack traces

errors

The errors does not support stack traces in 1.13.

Error wrapping

pkg/errors

errors.Wrap(err, "this is an error example")
  • errors.Wrap() returns nil when err is nil.

xerrors

xerrors.Errorf("this is an error example: %w", err)
  • : %w must be placed at the end of a string for xerrors.Errorf()
    • if you don’t follow this rule, a wrapping is ignored without any warning. This is a disadvantages to use xerrors package for error handling. We may need to introduce a lint to check violations for this rule.
  • xerrors.Errorf() returns errors when err is nil.

    • nil check is necessary before xerrors.Errorf(). Otherwise, error chains won’t work correctly.

      package main
      
      import (
      	"fmt"
      	"os"
      
      	"golang.org/x/xerrors"
      )
      
      func correct_func() error {
      	var err error = nil
      	if err != nil {
      		return xerrors.Errorf("this is an error example: %w", err)
      	}
      	return nil
      }
      
      func wrong_func() error {
      	var err error = nil
      	return xerrors.Errorf("this is an error example: %w", err) // this part does not return nil
      }
      
      func main() {
      	err := correct_func()
      	if err != nil {
      		fmt.Printf("Error in correct_func(): %+v", err)
      		os.Exit(1)
      	}
      
      	err = wrong_func()
      	if err != nil { // this part will be ignored due to the lack of nil check in wrong_func()
      		fmt.Printf("Error in wrong_func(): %+v", err)
      		os.Exit(1)
      	}
      
      	fmt.Println("Succeeded!!")
      	os.Exit(0)
      }
      
      //> go run main.go
      //Error in wrong_func(): this is an error example: %!w(<nil>):
      //    main.wrong_func
      //        /tmp/sandbox339401097/prog.go:20

errors

errors.Errorf("this is an error example: %w", err)
  • : %w can be placed anywhere for errors.Errorf()

Unwrap method(traversing error cause)

pkg/errors

errors.Cause(err)

xerrors

xerrors.Unwrap(err)

errors

errors.Unwrap(err)

Value comparison

pkg/errors

err = errors.Cause(err)
if err == ErrorA {
	// implementation for ErrorA
}

xerrors

if xerrors.Is(err, ErrorA) {
	// implementation for ErrorA
}

errors

if xerrors.Is(err, ErrorA) {
	// implementation for ErrorA
}

Type comparison

pkg/errors

if errA, ok := err.(*ErrorA); ok {
	// implementation for ErrorA
}

xerrors

var errA *ErrorA
if xerrors.As(err, &errA); ok {
	// implementation for ErrorA
}

errors

var errA *ErrorA
if errors.As(err, &errA); ok {
	// implementation for ErrorA
}

Conclusion

In golang 1.13, the error standard pacakge cannot be alternatives of pkg/errors and xerrors due to the lack of a stack tracing feature. If you use pkg/errors and xerrors for your products, you don’t have to be rush to migrate to the standard library at this moment. In addition, you don’t have to migrate from pkg/errors to xerrors because there are clear differences between syntaxes, stack trace behavior and the return value of their wrapping methods. I personally like using pkg/errors and will continue to use the package for my private projects.

References