Returning and Checking Errors in Go
How to return errors from your own functions, check them with if err != nil, and keep error handling readable with early returns.
Every Go program you write will contain the same three lines many times:
if err != nil {
return err
}
This page explains how that pattern works, how to write functions that return errors properly, and how to keep the checks readable instead of letting them pile up.
A Function That Returns an Error
Start with a function that can fail. It divides two numbers and refuses to divide by zero:
package main
import (
"errors"
"fmt"
)
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("cannot divide by zero")
}
return a / b, nil
}
func main() {
result, err := divide(10, 2)
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Println("result:", result)
_, err = divide(1, 0)
if err != nil {
fmt.Println("error:", err)
}
}
The signature (float64, error) tells every caller that this function can fail. On success it returns the answer and nil. On failure it returns a zero value for the number, plus an error. Output:
result: 5
error: cannot divide by zero
The zero value is what Go uses for "nothing": 0 for numbers, "" for strings, nil for pointers, slices and maps. When you return an error, fill the other results with zero values so nobody is tempted to use them.
The if err != nil Check
Right after calling a function that returns an error, you check it. You have three common choices of what to do with a non-nil error:
- Return it to your own caller, usually with added context. This is the most common choice.
- Handle it right there: retry, use a default value, or skip the item.
- Stop the program, but only in
mainor setup code, for example withlog.Fatal.
Here is the "use a default" choice. A missing port setting is fine, the program falls back to 8080:
package main
import (
"fmt"
"strconv"
)
func portFrom(s string) int {
p, err := strconv.Atoi(s)
if err != nil {
return 8080
}
return p
}
func main() {
fmt.Println(portFrom("3000"))
fmt.Println(portFrom(""))
}
This prints 3000 and then 8080. The error was handled on purpose, so it does not go any further.
Early Returns Keep the Happy Path on the Left
Go code handles each error and returns immediately, so the successful path continues down the page without extra indentation. Compare these two shapes.
Nested style, hard to read:
func load(path string) (Config, error) {
data, err := os.ReadFile(path)
if err == nil {
cfg, err := parse(data)
if err == nil {
return cfg, nil
} else {
return Config{}, err
}
} else {
return Config{}, err
}
}
Early-return style, the Go way:
func load(path string) (Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return Config{}, err
}
cfg, err := parse(data)
if err != nil {
return Config{}, err
}
return cfg, nil
}
Both do the same thing. In the second, each failure exits right away and the success path reads straight down. Reviewers in most Go teams will ask you to write the second form.
This shape is what you are aiming for:
If your function reads as "do step, check, do step, check, return result", with no else after an error check, you are writing idiomatic Go error handling.
Declaring err Inside the if Statement
When a function returns only an error, you can call it and check it in one line. The err variable then exists only inside the if:
package main
import (
"fmt"
"os"
)
func main() {
if err := os.Mkdir("/root/not-allowed", 0o755); err != nil {
fmt.Println("mkdir failed:", err)
}
if err := os.Remove("does-not-exist.txt"); err != nil {
fmt.Println("remove failed:", err)
}
}
Run as a normal user, this prints two failures, for example mkdir failed: mkdir /root/not-allowed: permission denied and remove failed: remove does-not-exist.txt: no such file or directory. The short form keeps err from leaking into the rest of the function, so you cannot accidentally check a stale err later.
Reusing err with := and =
The first time you get an error in a function you usually write x, err := f(). Later calls can keep using := as long as at least one variable on the left is new:
data, err := os.ReadFile(path) // declares data and err
cfg, err := parse(data) // declares cfg, reuses err
If nothing on the left is new, you must use =, or the compiler stops with:
no new variables on left side of :=
Shadowing err inside a block:
Writing x, err := f() inside an if or for block creates a new err that hides the outer one. Code after the block then checks the outer err, which never changed. If you need the outer variable, declare x first and assign with =. The shadow analyzer from golang.org/x/tools can find these for you.
Ignoring an Error on Purpose
Sometimes an error truly does not matter. You can drop it with the blank identifier _:
n, _ := fmt.Println("hello")
Do this rarely, and only when you are sure. Silently dropped errors are one of the hardest kinds of bug to find, because nothing is printed and the program carries on with bad data. If you ignore an error, a short comment saying why helps the next reader.
Returning an Error From main
main cannot return an error. The usual pattern is to put the real work in a run function that does return one, and let main print it and exit with a non-zero status:
package main
import (
"fmt"
"os"
"strconv"
)
func run(args []string) error {
if len(args) < 2 {
return fmt.Errorf("usage: %s NUMBER", args[0])
}
n, err := strconv.Atoi(args[1])
if err != nil {
return err
}
fmt.Println("double:", n*2)
return nil
}
func main() {
if err := run(os.Args); err != nil {
fmt.Fprintln(os.Stderr, "error:", err)
os.Exit(1)
}
}
Running go run . 21 prints double: 42. Running go run . abc prints error: strconv.Atoi: parsing "abc": invalid syntax to standard error and exits with status 1, which scripts and CI systems treat as failure. Keeping logic in run also makes it easy to test, because tests can call run and check the returned error.
Summary
Return error as the last result, return zero values next to it on failure, and return nil on success. Check every error right after the call and leave early, so the success path stays flat and readable. Use the one-line if err := f(); err != nil form when a function only returns an error, watch for := creating a shadowed err inside blocks, and only ignore an error with _ when you can explain why it is safe.