Dev Skills

Functions, multiple returns, and errors

There are no exceptions. An error is a value you return, and that changes everything.

Topic 9 of 98 · 5 min to read · 45 min to build
After this topic you can
  • Write a function that returns a result and an error, and check the error at the call site.
  • Explain why Go returns errors as values instead of throwing exceptions.
  • Write a variadic function and a closure that keeps its own state.

What a function is, and what an error is

A function is a named piece of code you can run from somewhere else, optionally giving it values (parameters) and optionally getting values back (return values). Breaking a program into functions is how you stop repeating yourself and how you give names to the steps of what you are doing.

Go functions can return more than one value, and errors travel that way.

An error is anything that stopped a function doing its job: a file that is not there, a network that did not answer, input that made no sense. Many languages use exceptions, which jump out of your function to a handler somewhere else. Go does not. An error is an ordinary value that gets returned alongside the result, and you check it right there.

Functions can return more than one value

The usual shape is a result plus an error as the last return value. Almost every function in the standard library that can fail looks like this.

funcs.go
func add(a, b int) int { // a and b are both int
return a + b
}
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("divide by zero")
}
return a / b, nil // nil means "no error"
}

The pattern you will write ten thousand times

main.go
res, err := divide(10, 0)
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Println(res)
Check the error, handle it, return early. The happy path stays on the left margin.

People coming from Java or Python find this verbose, and they are right: it is more lines than a try/catch. What you get for those lines is this. You can see every place a function can fail by reading it. No hidden control flow jumps out of the middle of a function into a handler three files away.

It also pushes you to decide at every failure. Once you write res, err :=, the compiler rejects an err you never use, and the escape hatch _ stands out in review. It is not a guarantee: calling divide(10, 0) as a bare statement drops both results and still compiles. Linters such as errcheck catch that case.

Watch out: Do not pass a bare return err up six layers. Add context: return fmt.Errorf("loading user %d: %w", id, err). Error values in depth covers what %w does. Without context, a production error reads "not found" and you have no idea what was not found.

Variadic functions

variadic.go
// inside the function, nums is a []int
func sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
// calling it, inside main or any other function:
fmt.Println(sum(1, 2, 3)) // 6
xs := []int{1, 2, 3}
fmt.Println(sum(xs...)) // 6 — "..." spreads a slice into the arguments
Excerpt: the last three lines go inside a function body. `[]int{1, 2, 3}` is a slice, a growable list (preview — explained in Arrays and slices).

Functions are values, and closures capture

A function can be assigned to a variable, passed as an argument, and returned. A function defined inside another one can see that outer function’s variables, and keeps them alive after it returns. That is a closure.

closure.go
counter := func() func() int {
c := 0 // c lives on, captured by the returned function
return func() int {
c++
return c
}
}()
fmt.Println(counter(), counter(), counter()) // 1 2 3

Try it yourself

press Run — this executes on the Go Playground
package main
import (
"errors"
"fmt"
)
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("divide by zero")
}
return a / b, nil
}
func main() {
if res, err := divide(10, 2); err != nil {
fmt.Println("error:", err)
} else {
fmt.Println("10 / 2 =", res)
}
if _, err := divide(10, 0); err != nil {
fmt.Println("error:", err) // the branch you must not forget
return
}
fmt.Println("never printed: we returned early")
}
Multiple returns, the error check, and returning early when something fails.
The short version
  • Errors are ordinary return values. There are no exceptions and no hidden jumps.
  • if err != nil { return ... } right after the call. Handle it and return early.
  • Always add context when you pass an error up. A bare return err loses the trail.
  • Functions are values: they can be passed, returned, and can capture their surroundings.
Remember: An error is a value, not an event. Every failure point is visible in the code you are reading.
Build this before moving on
Write func stats(nums ...int) (min, max int, avg float64, err error). Return an error for no arguments, and handle it at the call site.
Coding challenge · checked by tests
Write Average(nums ...int) (float64, error). It returns the average of the numbers. With no numbers at all it returns 0 and an error, because there is nothing to average. Example: Average(1, 2) → 1.5, nil. Average() → 0, an error.

Tab indents. Press Esc, then Tab, to leave the editor.

Then try these in your own editor
  1. 1Write a function returning two values, then ignore one with _ and decide if you would flag that in review.
  2. 2Write a closure counter, call it three times, then make a second one and confirm they are independent.
  3. 3Use a variadic function with both a list of arguments and a spread slice.
Check yourselfTake the Core language test