Returning and Checking Errors
Functions return an error next to their result, so every failure is a value you check with if err != nil before trusting the rest.
value, err := Operation(); if err != nil { return err }Go has no exceptions. Nothing is thrown and nothing is caught somewhere far away. A function that can fail returns its result and an error together, and the caller decides what to do. That is why if err != nil appears on nearly every page of Go code.
Two results, checked in order
By convention the error is the last result. Check it before anything else. When it is not nil, the other results hold the zero value of their type: 0 for a number, an empty string, nil for a pointer, slice or map. They are not left holding an older value, and they are not usable.
Handle it, or pass it up
An error has two honest endings. You handle it here, by retrying, using a default or telling the person. Or you return it to your caller. Returning is the common case. Near the top, usually in main, one piece of code prints the error and stops the program.
The shorthand
When you need only the error, the if statement can declare it. if err := save(u); err != nil { is exactly the same as err := save(u) followed by if err != nil {. The only difference is scope: in the short form err exists only inside the if and its else.
What it costs
This is more typing than an exception, and it buys visibility. Every call that can fail says so in the source. Taking only one of two results is a compile error, so discarding the error has to be written down as _.
Example
package main
import (
"errors"
"fmt"
)
// withdraw returns the new balance and an error. On failure the balance result
// is 0, so the caller must check err before using it.
func withdraw(balance, amount int) (int, error) {
if amount <= 0 {
return 0, errors.New("amount must be positive")
}
if amount > balance {
// Errorf formats values into the message, like Printf does.
return 0, fmt.Errorf("cannot withdraw %d from a balance of %d", amount, balance)
}
return balance - amount, nil
}
func main() {
balance := 100
for _, amount := range []int{30, 0, 500} {
newBalance, err := withdraw(balance, amount)
if err != nil {
// Note that newBalance is 0 here, not the old balance.
fmt.Println("rejected:", err)
continue
}
balance = newBalance
fmt.Println("balance now", balance)
}
}
// Output:
// balance now 70
// rejected: amount must be positive
// rejected: cannot withdraw 500 from a balance of 70When to use it
- Report back to a caller that the amount they asked to withdraw is too large.
- Stop a program with a clear message and a non-zero exit status when startup fails.
- Decide whether to retry a step or give up, based on the error a helper returned.
More examples
The same check written long and short
Both forms appear constantly in real code, so you need to read either one. Choose the short form when the error is the only thing the call produces, and the long form when you also need the value below the if.
// Longhand: one statement to call, one to test. err stays in scope for the
// rest of the function.
err := save(u)
if err != nil {
return err
}
// Shorthand: the if statement declares err itself. err exists only inside the
// if and its else. These two forms compile to the same thing.
if err := save(u); err != nil {
return err
}
// Use the longhand when you still need the value afterwards, because this does
// not compile: n is gone after the closing brace.
if n, err := count(); err != nil {
return err
}
// fmt.Println(n) // undefined: nmain is where errors stop
Every error that is passed up has to stop somewhere. The usual shape is a run function returning error and a tiny main that reports it. Note that os.Exit ends the program immediately and does not run deferred functions, which is why the exit lives in main and not in run. The message goes to standard error, so piping standard output to a file still shows it.
package main
import (
"fmt"
"os"
)
// run does the real work and returns an error instead of exiting, so it stays
// testable and its deferred clean-up still runs.
func run(args []string) error {
if len(args) < 2 {
return fmt.Errorf("usage: %s FILE", args[0])
}
fmt.Println("would process", args[1])
return nil
}
func main() {
// One place prints the error and sets the exit status.
if err := run([]string{"tool"}); err != nil {
fmt.Fprintln(os.Stderr, "error:", err)
os.Exit(1)
}
}
// Output:
// error: usage: tool FILEIgnoring an error has to be deliberate
Go will not let you silently take one of two results, so a dropped error is always visible as an underscore or a bare call. Dropping one is sometimes right, for instance closing a file you only read, but the comment is what stops the next reader from treating it as a bug.
// A compile error: count returns two values, not one.
// n := count()
// Legal, and the usual way to say "I only want the value". Every reader can
// see the error was dropped on purpose.
n, _ := count()
// Legal as a statement on its own: both results are discarded.
count()
// When you really mean to ignore it, say why.
// The file is read-only here, so a close failure cannot lose data.
defer f.Close()
Discussion