Errors as values

Return errors

Go has no exceptions: a function that can fail returns its result together with an error value. Success carries nil, and failure carries a value explaining what went wrong.

package main import ( "errors" "fmt" ) func divide(a float64, b float64) (float64, error) { if b == 0 { return 0, errors.New("division by zero") } return a / b, nil } func main() { fmt.Println(divide(7, 2)) fmt.Println(divide(7, 0)) }

go run main.go

3.5 <nil>
0 division by zero

Errors are ordinary values, so they travel through variables, struct fields, and return statements like any other data. The errors.New helper builds a simple error from a message string.

Check them immediately

Handle the error before touching the result. The standard shape is one if statement right after the call: on failure report the problem or return early, otherwise continue with the value.

package main import ( "errors" "fmt" ) func divide(a float64, b float64) (float64, error) { if b == 0 { return 0, errors.New("division by zero") } return a / b, nil } func main() { result, err := divide(7, 0) if err != nil { fmt.Println("oops:", err) } else { fmt.Println(result) } }

oops: division by zero

Linters flag unchecked errors, and reviewers will too: every function that returns an error expects the caller to look at it. Returning the error upward is fine when the current level has nothing useful to add.

Wrap with %w

Add context while keeping the original error reachable by wrapping it with fmt.Errorf and the %w verb. Callers can then match the root cause with errors.Is even through several layers of context.

package main import ( "errors" "fmt" ) var errEmpty = errors.New("empty name") func greet(name string) error { if name == "" { return fmt.Errorf("greet failed: %w", errEmpty) } fmt.Println("Hello,", name) return nil } func main() { err := greet("") fmt.Println(err) fmt.Println(errors.Is(err, errEmpty)) }

greet failed: empty name
true

Use %w for the cause and %v or %s for plain details: only %w links the chain for errors.Is and errors.As. One wrap per level is enough, at the point where fresh context appears.

Custom error types

Anything that implements the error interface with an Error method returning a string counts as an error. Custom types let failures carry structured data such as status codes instead of plain text.

package main import "fmt" type HTTPError struct { Code int Msg string } func (e HTTPError) Error() string { return fmt.Sprintf("http %d: %s", e.Code, e.Msg) } func fetch(code int) error { if code != 200 { return HTTPError{Code: code, Msg: "bad response"} } return nil } func main() { fmt.Println(fetch(200)) fmt.Println(fetch(404)) }

<nil>
http 404: bad response

Keep custom errors in the package that produces them so callers can match them with errors.As and read their fields. Start with errors.New and grow into custom types only when callers need more than a message.

Next: Pointers

Article author: Arthur Isaev