errors

package module
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 23, 2026 License: BSD-3-Clause Imports: 4 Imported by: 0

README

Errors

A drop-in replacement for the errors standard library with some additional functionality.

go get sjdaws.com/pkg/errors

Using this package

Full documentation is available on go.dev.

Note: A PublicError has all the functionality of a TraceableError.

Standard functions

This package contains several pass-through functions to achieve compatibility with the errors standard library.

Compatible functions

This package also contains several functions that are backwards compatible with the errors standard library but behave differently.

  • New1 creates a new TraceableError instance.
  • TraceableError.Error returns an error message prepended with context as a string.

1 New has the same signature as fmt.Errorf allowing verbs to be formatted using operands.

Additional functions

This package also contains several functions that provide additional functionality.

  • Public wraps an error in a PublicError instance.
  • PublicError.Code returns the error code as an integer.
  • PublicError.Message returns all error messages as a string.
  • PublicError.Messages returns all error messages as a string slice.
  • TraceableError.Trace returns a stack trace for an error as a string.
  • TraceableError.Unwrap returns the previous error in an error stack.
  • TraceableError.Wrap wraps an error in another error with additional context.

Examples

  1. Traceable errors
  2. Public errors
Traceable errors

A lot of go code returns an error object as is:

value, err := function()
if err != nil {
	return err
}

Most loggers will add context about where the error was logged, and panics will include context about where the panic occurred, however tracing the origin of an error can become difficult, especially as project complexity increases and errors are passed through many layers of an application.

This example will start with a very simple application that increases in complexity to illustrate how debugging the origin of an error can become difficult and how traceable errors can help.

Single function

Initially, the application has one function which panics.

Code

Run this code in the Go Playground

// main.go
package main

func main() {
	panic("oh no")
}
Output
panic: oh no

goroutine 1 [running]:
main.main()
	/go/main.go:5 +0x1
exit status 2

This is very easy to debug as the panic provides useful information, specifically:

  • main.main() points to the function main() within the main package
  • /go/main.go:5 points to line 5 in the file /go/main.go

There is only one step to debugging this panic:

  • The panic occurred on line 5 because that is all the application does
Two functions

The application now has a second function which returns an error.

Code

Run this code in the Go Playground

// main.go
package main

import (
	"fmt"
	"strconv"
)

func main() {
	integer, err := toInteger("A")
	if err != nil {
		panic(err)
	}

	fmt.Printf("Integer value: %d\n", integer)
}

func toInteger(value string) (int, error) {
	integer, err := strconv.Atoi(value)
	if err != nil {
		return 0, err
	}

	return integer, nil
}
Output
panic: strconv.Atoi: parsing "A": invalid syntax

goroutine 1 [running]:
main.main()
	/go/main.go:12 +0x1
exit status 2

Debugging this panic requires an extra step, but it is still very simple:

  • The panic occurred on line 12 when the call to toInteger on line 10 returned an error
  • toInteger returned an error on line 21 because the call to strconv.Atoi on line 19 returned an error
Multiple error origins

The application has changed so toInteger can handle a wider range of inputs, as a side effect a switch statement has been added to handles different types.

Code

Run this code in the Go Playground

// main.go
package main

import (
	"fmt"
	"strconv"
)

func main() {
	integer, err := toInteger([]string{"A"})
	if err != nil {
		panic(err)
	}

	fmt.Printf("Integer value: %d\n", integer)
}

func toInteger(value any) (int, error) {
	var integer int

	var err error

	switch (value).(type) {
	case int:
		integer = value.(int)

	case []string:
		integer, err = stringSliceToInteger(value.([]string))

	case string:
		integer, err = stringToInteger(value.(string))

	default:
		return 0, fmt.Errorf("unable to convert type '%T' to integer", value)
	}

	return integer, err
}

func stringToInteger(value string) (int, error) {
	integer, err := strconv.Atoi(value)
	if err != nil {
		return 0, err
	}

	return integer, nil
}

func stringSliceToInteger(value []string) (int, error) {
	integer, err := strconv.Atoi(value[0])
	if err != nil {
		return 0, err
	}

	return integer, nil
}
Output
panic: strconv.Atoi: parsing "A": invalid syntax

goroutine 1 [running]:
main.main()
	/go/main.go:12 +0x1
exit status 2

Without debating the quality of the code, this is where debugging can start to become a bit more difficult:

  • The panic occurred on line 12 when the call to toInteger on line 10 returned an error
  • toInteger returned an error on line 37 because either the call to stringSliceToInteger on line 28 returned an error or the call to stringToInteger on line 31 returned an error

It's difficult to tell from the error message which function the error originated from, or what the original data in was.

Wrapping errors in context

The errors can be wrapped using this package to provide additional context along with the original error message.

Code

Run this code in the Go Playground

// main.go
package main

import (
	"fmt"
	"strconv"

	"sjdaws.com/pkg/errors"
)

func main() {
	integer, err := toInteger([]string{"A"})
	if err != nil {
		panic(err)
	}

	fmt.Printf("Integer value: %d\n", integer)
}

func toInteger(value any) (int, error) {
	var integer int

	var err error

	switch (value).(type) {
	case int:
		integer = value.(int)

	case []string:
		integer, err = stringSliceToInteger(value.([]string))

	case string:
		integer, err = stringToInteger(value.(string))

	default:
		return 0, fmt.Errorf("unable to convert type '%T' to integer", value)
	}

	if err != nil {
		return 0, errors.Wrap(err, "unable to convert value '%s' to integer", value)
	}

	return integer, err
}

func stringToInteger(value string) (int, error) {
	integer, err := strconv.Atoi(value)
	if err != nil {
		return 0, err
	}

	return integer, nil
}

func stringSliceToInteger(value []string) (int, error) {
	integer, err := strconv.Atoi(value[0])
	if err != nil {
		return 0, err
	}

	return integer, nil
}
Output
panic: unable to convert string slice '[A]' to integer: strconv.Atoi: parsing "A": invalid syntax

goroutine 1 [running]:
main.main()
	/go/main.go:14 +0x1
exit status 2

The additional context in the error message unable to convert string slice '[A]' to integer: provides information about the data which was being converted. In this case the value was a slice and therefore the error was thrown from stringSliceToInteger.

Using this information, debugging is relatively straight forward again:

  • The panic occurred on line 12 when the call to toInteger on line 10 returned an error
  • toInteger returned an error on line 37 because the call to stringSliceToInteger on line 28 returned an error
  • stringSliceToInteger returned as error on line 58 because the call to strconv.Atoi on line 56 returned an error
Tracing wrapped errors

Invoking Error() on this package prefixes the original error message with the additional context added when the error occurred. While this is often enough to debug simple errors, it may not be sufficient for complex applications where an error can traverse many layers, especially if data is being manipulated along the way.

A wrapped error is like an onion, where the middle is the original error, and each wrapping adds an additional layer outside. Wrapping and re-wrapping errors as it makes its way back through the stack provides a much better insight on what caused the error in the first place. Wrapping essentially provides telemetry for logged errors.

Wrapped errors from this package have a Trace() function which will unwrap the error and print out the context added to each layer to assist with understanding the cause of the error and the path used to invoke it.

Code

Run this code in the Go Playground

// main.go
package main

import (
	"fmt"
	"strconv"

	"sjdaws.com/pkg/errors"
)

func main() {
	integer, err := toInteger([]string{"A"})
	if err != nil {
		if errors.As(err, &errors.TraceableError{}) {
			panic(err.(errors.TraceableError).Trace())
		}

		panic(err)
	}

	fmt.Printf("Integer value: %d\n", integer)
}

func toInteger(value any) (int, error) {
	var integer int

	var err error

	switch (value).(type) {
	case int:
		integer = value.(int)

	case []string:
		integer, err = stringSliceToInteger(value.([]string))

	case string:
		integer, err = stringToInteger(value.(string))

	default:
		return 0, fmt.Errorf("unable to convert type '%T' to integer", value)
	}

	if err != nil {
		return 0, errors.Wrap(err, "unable to convert value '%s' to integer", value)
	}

	return integer, err
}

func stringToInteger(value string) (int, error) {
	integer, err := strconv.Atoi(value)
	if err != nil {
		return 0, errors.Wrap(err, "unable to convert string '%s' to integer", value)
	}

	return integer, nil
}

func stringSliceToInteger(value []string) (int, error) {
	integer, err := strconv.Atoi(value[0])
	if err != nil {
		return 0, errors.Wrap(err, "unable to convert string slice '%v' to integer", value)
	}

	return integer, nil
}
Output
panic: strconv.Atoi: parsing "A": invalid syntax
	- /go/main.go:44: unable to convert value '[A]' to integer
	- /go/main.go:62: unable to convert string slice '[A]' to integer

goroutine 1 [running]:
main.main()
	/go/main.go:12 +0x1
exit status 2

From the trace it's easy to see the path of the error. The last item in the stack shows the error originated on line 62 in /go/main.go. The context added with each wrapper is also shown, in this case additional context was added to the error returned on line 44 in /go/main.go.

Because context follow the same format as fmt.Sprintf it can be used to show what data was passed and what data was received throughout the stack.

Public errors

Because errors only contain a single message, they're often masked from end users to ensure potentially sensitive information isn't exposed. This offloads the messaging to the UX layer which either creates strong coupling between the UX layer and the package throwing the error or returning vague error messages, and in some cases both.

// Strongly coupled with package for error comparison
if errors.As(err, &package.NotFoundError{}) {
	return "Page not found"
}

// Vague messaging for all other errors
return "An error occurred"

In some systems there may be entire tables of comparisons with error codes, error messages, or error types to determine the appropriate error to return to the end user. This package attempts to simplify this problem by providing a wrapper that works in a similar way to error wrapping allowing the end user error messages to be set by the package throwing the error.

This example will show how to wrap an error message so the internal context isn't lost while maintaining useful error messages for end users.

For the sake of simplicity, this example prints to stdout and everything is contained in the main package. This is an emulation of a real world database package and responding with an error message in a http response or similar.

Authentication failure

This application attempts to authenticate a user in a database, but the password is invalid.

Code

Run this code in the Go Playground

// main.go
package main

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

func main() {
	db := Connect()

	user, err := db.Login("bob", "passwrod")
	if err != nil {
		fmt.Println(handleError(err))

		os.Exit(1)
	}

	fmt.Printf("Hi %s!\n", user["name"])
}

func handleError(err error) string {
	log.Println(err)

	return fmt.Sprintf("ERROR: %s\n", err)
}

// Emulated database.go package
var ErrInvalidPassword = errors.New("the password entered is incorrect")

var ErrInvalidUsername = errors.New("the username entered can not be found")

type DB struct{}

func Connect() *DB {
	return &DB{}
}

func (d *DB) Login(username string, password string) (map[string]any, error) {
	user := map[string]any{
		"id":   1,
		"name": "bob",
	}

	if username != "bob" {
		return user, ErrInvalidUsername
	}

	if username == "bob" && password != "password" {
		return user, ErrInvalidPassword
	}

	return user, nil
}
Output
ERROR: the password entered is incorrect

2000/01/01 00:00:00 the password entered is incorrect

Process finished with the exit code 1

Like most applications, the calling function doesn't know why the error occurred, but it has to handle the error it received. In this example the error is being printed back to the end user as-is, however the error message contains information that could be used to perform account enumeration. The error is useful for the application, such as for debugging or rate limiting incorrect logins, but opens a potential attack vector if displayed to the end user.

To mitigate this the calling function needs to either handle the error and provide an appropriate error message or display a generic error message as per the intro to this example.

Public messaging

Using this package, the database package can set a separate internal and public message.

Code

Run this code in the Go Playground

// main.go
package main

import (
	"fmt"
	"log"
	"os"

	"sjdaws.com/pkg/errors"
)

func main() {
	db := Connect()

	user, err := db.Login("bob", "passwrod")
	if err != nil {
		fmt.Println(handleError(err))

		os.Exit(1)
	}

	fmt.Printf("Hi %s!\n", user["name"])
}

func handleError(err error) string {
	log.Println(err)

	if errors.As(err, &errors.PublicError{}) {
		return fmt.Sprintf("ERROR: %v\n", err.(errors.PublicError).Message())
	}

	return fmt.Sprintln("An error occurred.")
}

// Emulated database.go package
var ErrInvalidPassword = errors.Public(errors.New("the password entered is incorrect"), 403, "Authentication failed.")

var ErrInvalidUsername = errors.Public(errors.New("the username entered can not be found"), 403, "Authentication failed.")

type DB struct{}

func Connect() *DB {
	return &DB{}
}

func (d *DB) Login(username string, password string) (map[string]any, error) {
	user := map[string]any{
		"id":   1,
		"name": "bob",
	}

	if username != "bob" {
		return user, ErrInvalidUsername
	}

	if username == "bob" && password != "password" {
		return user, ErrInvalidPassword
	}

	return user, nil
}
Output
ERROR: Authentication failed.

2000/01/01 00:00:00 the password entered is incorrect

Process finished with the exit code 1

Changing the error type allows the application to use the error to perform logic, without having to handle how it will be displayed to an end user.

Additionally, public errors also allow many errors messages to be added to a single error, which is useful for items such as validation where the UX layer is able to format error sets.

Documentation

Overview

Package errors provides better error tracing and separates internal/public messaging.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func As

func As(err error, target any) bool

As passthrough to errors.As. See https://pkg.go.dev/errors#As.

func AsType

func AsType[E error](err error) (E, bool)

AsType passthrough to errors.AsType. See https://pkg.go.dev/errors#AsType.

func Is

func Is(err error, target error) bool

Is passthrough to errors.Is. See https://pkg.go.dev/errors#Is.

func New

func New(message any, replacements ...any) error

New creates a new TraceableError error instance.

func Public

func Public(err error, code int, message ...string) error

Public wraps an error in public error message.

func Wrap

func Wrap(err error, context any, replacements ...any) error

Wrap wraps an error with additional context.

Types

type PublicError

type PublicError struct {
	TraceableError
	// contains filtered or unexported fields
}

PublicError errors which can be exposed to end users.

func (PublicError) Code

func (p PublicError) Code() int

Code returns the error code passed to Public.

func (PublicError) Message

func (p PublicError) Message(delimiter ...string) string

Message returns the messages passed to Public as a single string.

func (PublicError) Messages

func (p PublicError) Messages() []string

Messages returns the messages passed to Public.

type TraceableError

type TraceableError struct {
	// contains filtered or unexported fields
}

TraceableError error interface compatible error type which supports tracing.

func (TraceableError) Error

func (e TraceableError) Error() string

Error provides the last context message and original error message.

func (TraceableError) Trace

func (e TraceableError) Trace() string

Trace returns an error message with full stack trace.

func (TraceableError) Unwrap

func (e TraceableError) Unwrap() error

Unwrap returns the next error in the error stack.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL