How to Return an Error from a Function in Go

Return errors in Go by adding 'error' to the function signature and returning an error value created with errors.New or fmt.Errorf.

When the function fails

You write a function to parse a user's age from a string. You test it with "25" and it works. You deploy. A user enters "twenty-five". The program panics and crashes because your function returned a zero value and the caller assumed success. In Go, functions don't throw exceptions. They return errors. The caller has to check them. If the caller forgets, the bug hides until runtime.

This design forces explicit error handling. There is no try-catch block that swallows failures silently. The error flows back through the return values, and the code must deal with it. This verbosity is intentional. It makes the failure path visible in every function call.

Errors are values

An error in Go is just a value, like an integer or a string. The type is error, which is an interface defined in the standard library. The interface has one method: Error() string. Any type that implements this method satisfies the interface.

When a function succeeds, it returns nil for the error. nil is the zero value for interfaces. When a function fails, it returns a non-nil error value. The caller checks the error using if err != nil. This pattern is universal in Go. You will see it in every standard library function and every production codebase.

The compiler checks types, not logic. If you declare a function returns error, the compiler ensures every code path returns an error value. If you forget to return the error, the compiler rejects the program with not enough return values. If you return a string instead of an error, you get cannot use "message" (untyped string constant) as error value in return argument. The compiler catches these mistakes before the code runs.

Errors are values. Treat them like data, not exceptions.

The error interface

The error interface is simple. It requires a single method that returns a string. This simplicity allows flexibility. You can create errors using errors.New for static messages, fmt.Errorf for dynamic messages, or custom types for structured data.

The interface hides the concrete type. Callers only see the error type. They don't need to know whether the error came from the file system, the network, or your validation logic. This decoupling makes code easier to maintain. You can change the error implementation without breaking callers, as long as the interface is satisfied.

Custom error types are structs that implement the Error method. You define the struct with fields for additional data and add a method that formats the message. The receiver name for the method should be short, like e. For example, (e *ValidationError) Error() string. This follows the convention of using one or two letters for receiver names.

Custom errors carry data. Use them when the error needs structure.

Minimal example

Here's the simplest pattern: a function that returns a value and an error, and a caller that checks the error before using the value.

package main

import (
	"errors"
	"fmt"
)

// Divide returns the result of a / b.
// It returns an error if b is zero.
func Divide(a, b float64) (float64, error) {
	if b == 0 {
		// Return a descriptive error for division by zero.
		return 0, errors.New("division by zero")
	}
	return a / b, nil
}

func main() {
	result, err := Divide(10, 2)
	if err != nil {
		// Handle the error immediately.
		fmt.Println("error:", err)
		return
	}
	fmt.Println("result:", result)
}

The function Divide returns two values: the result and an error. If the divisor is zero, it returns 0 and an error created by errors.New. The caller captures both values. It checks if err != nil before using the result. This prevents using a zero value that might be indistinguishable from a valid result.

The errors.New function creates a simple error with a static message. It's useful for errors that don't need dynamic context. The message should be lowercase and have no punctuation. This is a community convention. Error messages are often embedded in larger messages by the caller, so capitalization and punctuation can look awkward.

Errors are values. Treat them like data, not exceptions.

Wrapping and context

Real code needs context. When a function calls another function that returns an error, you should wrap the error to add details about where it happened. Wrapping preserves the original error while adding a layer of context.

Use fmt.Errorf with the %w verb to wrap an error. The %w verb stores a reference to the underlying error. This reference allows the chain to be traversed later. The errors.Is function walks the chain to find a specific target error. The errors.As function walks the chain to extract a specific error type.

Wrapping creates a chain of errors. Each link adds context. The output of the error includes the full chain, making debugging easier. You can see the root cause and every layer that wrapped it. This mechanism replaces the need for error codes or exception types.

Wrap errors to preserve context. Unwrap them to inspect the cause.

Realistic example

Here's how wrapping works in a function that reads a file. The function wraps the OS error with the file path. The caller checks for a specific error using errors.Is.

package main

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

// LoadConfig reads the configuration file and returns an error if it fails.
func LoadConfig(path string) error {
	data, err := os.ReadFile(path)
	if err != nil {
		// Wrap the OS error with file path context.
		return fmt.Errorf("reading config %s: %w", path, err)
	}
	return nil
}

func main() {
	err := LoadConfig("config.yaml")
	if err != nil {
		// Check for specific error types using errors.Is.
		if errors.Is(err, os.ErrNotExist) {
			fmt.Println("config missing")
			return
		}
		fmt.Println("fatal:", err)
	}
}

The LoadConfig function calls os.ReadFile. If the read fails, it wraps the error with fmt.Errorf. The %w verb ensures the original error is preserved. The caller uses errors.Is to check if the error is os.ErrNotExist. This check works even though the error is wrapped. errors.Is walks the chain and finds the target.

This approach is better than string matching. String matching is fragile because messages can change. errors.Is compares error values, which provides a stable contract. Sentinel errors like os.ErrNotExist are exported variables that represent specific conditions. Functions return these variables when the condition occurs. Callers compare against them using errors.Is.

Sentinel errors enable precise control flow. String matching is a trap.

Sentinel errors

Sentinel errors are variables exported from a package that represent specific error conditions. For example, io.EOF is a sentinel error indicating end of file. Functions return this variable when the condition occurs. Callers compare the returned error against the sentinel using errors.Is.

Sentinel errors allow callers to handle specific cases without relying on string matching. They provide a stable contract. The package defines the sentinel, and callers import it. If the package changes the error message, the sentinel remains the same. Callers don't break.

Use a sentinel error variable when multiple functions need to check for the same specific error condition. Define the sentinel in the package where the error originates. Export it so callers can import it. Document the sentinel in the package documentation.

Sentinel errors enable precise control flow. String matching is a trap.

Custom errors

Sometimes you need to return structured data along with the error. A custom error type is a struct that implements the error interface. You define the struct with fields for the data and add an Error method that formats the message. Callers can use errors.As to extract the struct and access the fields.

This is useful for validation errors that need to report which field failed. Or for HTTP errors that need to include the status code. The custom error carries the data. Callers extract it when needed.

Define the struct with unexported fields to prevent modification. Add an Error method that returns a user-friendly message. Implement Unwrap if the error wraps another error. This allows errors.Is and errors.As to work correctly.

Custom errors carry data. Use them when the error needs structure.

Pitfalls and conventions

Common mistakes include returning nil error with a non-nil value, forgetting to return the error, or swallowing errors with _. The compiler catches missing returns. It rejects the program with not enough return values. If you return the wrong type, you get a type mismatch error.

Swallowing errors with _ is dangerous. result, _ := func() discards the error intentionally. Use this sparingly. Only discard errors when you have verified the operation cannot fail or the error is irrelevant. The community frowns on ignoring errors. The if err != nil { return err } pattern is verbose by design. The community accepts the boilerplate because it makes the unhappy path visible. Don't try to hide it.

Error messages should be lowercase and have no punctuation. This is a community convention. The error is often embedded in a larger message. Capitalization and punctuation can look awkward when combined. Follow the convention to keep messages consistent.

The error return value should always be the last return value. This is a convention. Functions return success values first, then the error. Callers check the error last. This makes the signature predictable.

Panic is for unrecoverable errors. Use panic only when the program cannot continue. Examples include missing configuration at startup or invariant violations. Errors are for expected failures. Use errors for I/O failures, validation errors, and network timeouts. Don't panic for recoverable conditions.

Errors are values. Treat them like data, not exceptions.

Decision matrix

Use errors.New when you need a simple, static error message with no dynamic context.

Use fmt.Errorf with %w when you want to wrap an underlying error so callers can unwrap it later.

Use fmt.Errorf without %w when you need to add context but the underlying error is not relevant to the caller.

Use a custom error type when you need to attach structured data or specific methods to the error.

Use a sentinel error variable when multiple functions need to check for the same specific error condition.

Use panic when the program has reached an unrecoverable state and cannot continue safely.

Where to go next