Creating Errors with errors.New and fmt.Errorf

The two everyday ways to create an error in Go, when to use each, and how to put useful details like IDs and file names into the message.

Before you can return an error, you need to make one. Go gives you two functions for this in the standard library: errors.New for a fixed message and fmt.Errorf for a message built from values. Almost every error you create by hand starts with one of them.

errors.New for a Fixed Message

errors.New takes a string and returns an error whose Error() method returns that string.

package main

import (
	"errors"
	"fmt"
)

func withdraw(balance, amount int) (int, error) {
	if amount <= 0 {
		return balance, errors.New("amount must be positive")
	}
	if amount > balance {
		return balance, errors.New("insufficient funds")
	}
	return balance - amount, nil
}

func main() {
	b, err := withdraw(100, 30)
	fmt.Println(b, err)

	b, err = withdraw(100, 500)
	fmt.Println(b, err)
}

Output:

70 <nil>
100 insufficient funds

Printing a nil error shows <nil>. That is handy while learning, but in real code you check err != nil instead of printing it.

errors.New is the right tool when the message never changes. If you need to put a number, name or file path into the message, use fmt.Errorf.

fmt.Errorf for Messages With Details

fmt.Errorf works like fmt.Sprintf: it takes a format string with verbs like %d, %s and %q, fills them in, and returns the result as an error.

package main

import (
	"fmt"
)

func findUser(id int, users map[int]string) (string, error) {
	name, ok := users[id]
	if !ok {
		return "", fmt.Errorf("user %d not found", id)
	}
	return name, nil
}

func main() {
	users := map[int]string{1: "asha", 2: "ravi"}

	if _, err := findUser(7, users); err != nil {
		fmt.Println(err)
	}
}

This prints user 7 not found. The ID in the message is what makes it useful. When this shows up in a log at 2 AM, "user not found" tells you very little, while "user 7 not found" tells you exactly where to look.

A few verbs are worth knowing for error messages:

  • %d for integers: user 7 not found
  • %s for strings: unknown command deploy
  • %q for strings with quotes around them, which makes empty or space-filled values visible: unknown command ""
  • %v for any value, using its default format

Prefer %q for user input:

With %s, an empty string disappears from the message and a value with trailing spaces looks normal. %q prints "" and "admin " so you can see exactly what was received.

Adding Context to an Error You Received

The most common use of fmt.Errorf is not creating a brand new error but adding context to one you got from another function. For that, Go has a special verb, %w:

package main

import (
	"fmt"
	"os"
)

func loadConfig(path string) ([]byte, error) {
	data, err := os.ReadFile(path)
	if err != nil {
		return nil, fmt.Errorf("load config: %w", err)
	}
	return data, nil
}

func main() {
	_, err := loadConfig("missing.yaml")
	fmt.Println(err)
}

Output:

load config: open missing.yaml: no such file or directory

Your text comes first, then : , then the original message. %w also keeps a link to the original error so that code further up can still recognize it. This is called wrapping. When you are passing on an error you received, use %w.

Where to Create Error Values

There are two places you can call errors.New:

Inside the function, every time it fails. This is fine when callers only need to print the message.

return errors.New("insufficient funds")

Once, at package level, as a named variable. This lets callers check for this exact error.

var ErrInsufficientFunds = errors.New("insufficient funds")

func withdraw(balance, amount int) (int, error) {
	if amount > balance {
		return balance, ErrInsufficientFunds
	}
	return balance - amount, nil
}

Each call to errors.New creates a distinct value, even with the same text. Two separate errors.New("insufficient funds") calls are not equal to each other. That is why a package-level variable is needed if callers should be able to test for the error. These named variables are called sentinel errors.

Same text does not mean same error:

errors.New("x") == errors.New("x") is false. Comparing errors only works when both sides are the same variable, such as ErrInsufficientFunds. If you need comparison, define the variable once and return it.

Writing Good Error Messages

A good message answers two questions: what were you trying to do, and what went wrong. Some rules that keep messages clean when they get chained together:

  • lowercase first letter: parse port: invalid syntax, not Parse port: Invalid syntax
  • no trailing period or newline
  • do not start with "error" or "failed to", because the reader already knows it is an error
  • include the input that caused it: the ID, path or value
  • do not repeat what the wrapped error already says

Compare these two chains for the same failure:

Error: failed to load config: failed to read file: open app.yaml: no such file or directory.
load config: open app.yaml: no such file or directory

The second is shorter and says just as much.

Format string with no arguments:

fmt.Errorf(msg) where msg is a variable is flagged by go vet with non-constant format string in call to fmt.Errorf. If msg contains a %, the output is garbled. Use errors.New(msg) for plain text, or fmt.Errorf("%s", msg).

Summary

Use errors.New for a fixed message and fmt.Errorf when the message needs values in it. When you pass on an error you received, wrap it with fmt.Errorf("doing X: %w", err) so the original stays reachable. If callers need to check for a specific error, create it once as a package-level variable, because two errors.New calls never compare equal. Keep messages lowercase, short and specific, with the input that caused the failure.