Skip to content
Theory · Errors · lesson 1 of 11

An error is a value

What you'll learn

What you'll learn:

  • explain why an error in Go is an ordinary value, and read signatures like (int, error);
  • create errors with errors.New and fmt.Errorf;
  • check an error right after the call with if err != nil;
  • wrap an error with %w and find it with errors.Is.
A courier hands a parcel to a recipient with a delivery note marked err lying on top of the box

Imagine you ordered a parcel. The courier hands you the box together with a delivery note. Usually the note is clean, but sometimes it carries a remark: "item damaged" or "wrong size". It makes sense to read the note first and only then open the box.

Go functions that may fail at their job work the same way. Not every string can be turned into a number or a bool. The strconv.ParseBool function from the strconv package understands "true" and "false", but it cannot parse the word "yes". It reports the failure through its second result—that is the delivery note:

GO
package main

import (
    "fmt"
    "strconv"
)

func main() {
    b, err := strconv.ParseBool("yes")
    fmt.Println(b, err) // false strconv.ParseBool: parsing "yes": invalid syntax
    b, err = strconv.ParseBool("true")
    fmt.Println(b, err) // true <nil>
}

Let's walk through the example line by line:

  1. The first call gets "yes" and returns false together with an error.
  2. The first Println prints both results. The error's text names the function, the string and the reason: invalid syntax.
  3. The second call parses "true" and returns true.
  4. The second Println prints true <nil>: the second result is <nil>—a clean note.

The program neither stops nor crashes. The error arrives in the variable err as an ordinary value, and the program decides what to do with it. When parsing succeeds, err holds nil.

A signature with an error

That the function can fail is visible from its signature alone. A signature is the line with the function's name, its parameters and its result types. For strconv.ParseBool it is func ParseBool(str string) (bool, error). Let's take it apart:

  • ParseBool—the function's name;
  • (str string)—the parameter: the string to parse;
  • (bool, error)—two results: the value itself and the error.

The second result of type error warns the calling code in advance, before the first run. The first result cannot be taken alone: a call with one variable on the left does not compile.

The error interface

The error type is built into the language, like int. Here is what you need to know about it:

  • it is an interface with one method: Error() string;
  • the method returns the error's text, and that is what fmt.Println prints;
  • any type with such a method can be a value of type error.

Here is an error type of your own and a function that returns it:

GO
type tooOld int

func (a tooOld) Error() string {
    return fmt.Sprintf("age %d is too big", int(a))
}

func checkAge(n int) (int, error) {
    if n > 150 {
        return 0, tooOld(n)
    }
    return n, nil
}

The checkAge function returns the value tooOld(n) where an error is expected. The compiler accepts it: tooOld has the method Error() string, so the type implements the interface. The standard library's errors are built the same way: each has a type with an Error() method.

nil means success

The zero value of an interface is nil. For an error it means "everything went fine", so checkAge returns n, nil for a valid age:

GO
age, err := checkAge(30)
fmt.Println(age, err) // 30 <nil>
age, err = checkAge(200)
fmt.Println(age, err) // 0 age 200 is too big

Let's go through both calls:

  1. checkAge(30) passes the check and prints 30 <nil>.
  2. checkAge(200) returns 0 and an error, and fmt.Println prints its text: age 200 is too big.

Two rules for results

Functions that return an error follow two rules:

  1. The error comes as the last result.
  2. If it is not nil, the other results are not relied on: they usually hold the zero values of their types.

That is why checkAge returns 0 on failure, not the age itself.

Here the parcel picture stops working. In real life you can sometimes take something intact out of a dented box. In Go, when there is an error, the contents of the box stay untouched: the note says they can't be trusted.

Comparing with nil

The value err is compared with nil the same way as a pointer:

  • err == nil is success;
  • err != nil is failure.

Reading the error's text is not needed for that. On success, return exactly nil.

This is a classic interview question: if you return an empty pointer of your own error type as an error, the calling code gets err != nil. As you already know from the interfaces theme, an interface holding a nil pointer is not equal to nil.

Common mistake

On failure, nil is returned in place of the number:

GO
func checkAge(n int) (int, error) {
    if n > 150 {
        return nil, tooOld(n) // ✗
    }
    return n, nil
}
TEXT
cannot use nil as int value in return statement

A pointer, a slice, a map or an interface can be nil, but a number cannot. For a result of type int, return its zero value: return 0, tooOld(n).

Key points

  • An error in Go is an ordinary value of type error; it comes as the last result.
  • error is an interface with one method, Error() string, and any type with that method implements it.
  • err == nil means success; when err != nil, the other results are not relied on.
  • On success return nil; on failure return the zero values of the other results, such as 0.

Subscribe to take the course

The course is free. Sign in with Telegram and subscribe to the channel to open every lesson.

Sign in another way