Errors

By the end of this lesson you'll handle failure the Go way — returning errors as ordinary values, checking them with if err != nil , wrapping them with context, and detecting the root cause through the whole chain. No exceptions, no surprises.

Learn Python, JavaScript, Java and more with free interactive lessons, real projects and AI-powered help. Beginner-friendly.

Part of the free Go course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

What You'll Learn in This Lesson

If you come from Java, Python, or C#, you're used to exceptions : a function throws , execution jumps somewhere far away, and you wrap risky code in try/catch . The danger is invisible — nothing in a function's signature tells you it can blow up, so it's easy to forget to catch.

Go made a deliberate choice: errors are values . A function that can fail says so in its return type (T, error) , and you handle the failure right where it happens with a normal if . It's more typing, but the failure is impossible to miss and the control flow is obvious — you can read it top to bottom. Go keeps panic only for truly exceptional, unrecoverable bugs.

1. Errors Are Values: (result, error)

A function that can fail returns two things: the result, and an error . By convention error comes last. On success you return a real value and nil ; on failure you return the zero value and a non- nil error. The caller then runs the cornerstone idiom — if err != nil — before trusting the result. Read this worked example and run it.

Your turn. The program below is almost complete — fill in the three ___ blanks using the // 👉 hints, then run it and check it against the expected output.

2. Sentinel Errors, Custom Types & Wrapping

A sentinel error is a single shared value (like ErrNotFound ) that callers compare against to react to a specific, known condition. A custom error type is any type with an Error() string method — use it when you need to carry extra data, like which field failed. To add context as an error travels up, wrap it with fmt.Errorf and the %w verb: that keeps the original detectable with errors.Is (match a sentinel) and errors.As (extract a type).

Now you try. Fill in the two blanks so the error wraps correctly and the root cause is still detectable.

%w wraps; %v just prints. fmt.Errorf("...: %w", err) embeds err so the chain stays inspectable. %v flattens it to text and the link to the original is gone — errors.Is can no longer find it. Use %w exactly once per Errorf when you want the cause preserved.

errors.Is answers "is this specific value (a sentinel) anywhere in the chain?" — errors.Is(err, ErrNotFound) . errors.As answers "is there an error of this type in the chain?" and, if so, copies it into your variable so you can read its fields — errors.As(err, &ve) .

3. Early Return & the Wrapping Chain

Idiomatic Go handles the error and returns early , which keeps the "happy path" flat and unindented at the bottom of the function — you rarely see deep else nesting. As the error passes up through layers, each one wraps it with its own context, building a readable breadcrumb trail. Crucially, the original cause is still detectable at the very top.

4. panic / recover — and When NOT To

panic aborts the normal flow and unwinds the stack — it's Go's closest thing to throwing. Reserve it for unrecoverable programmer bugs (an impossible state, a nil that should never occur), not for expected failures like bad user input — those are errors you return. recover , called inside a defer , stops a panic from crashing the process; libraries use it at a boundary to convert a panic back into an ordinary error.

Writing result, _ := mightFail() throws the error away and uses a possibly-invalid result. The go vet tool and linters flag this. Always assign err and check it: .

Calling panic(...) for an expected failure (bad input, missing file) crashes the program and can't be handled cleanly by the caller. Return an error instead, and let the top level decide what to do.

Using := inside an if or block declares a new err that only lives there, so the outer one is never updated and the function returns nil by mistake. Reuse the existing variable with = , or check the error in the same statement.

adds nothing useful. Always describe the operation that failed: .

fails once the error has been wrapped. Use , which walks the whole chain.

📋 Quick Reference

No blanks this time — just a brief and an outline. Write it, run it on the Go Playground, and check your output against the example in the comments.

Practice quiz

In Go, an error is...

  • a thrown exception
  • a panic
  • an ordinary value you return
  • a compiler directive

Answer: an ordinary value you return. Go has no exceptions; failable functions return an error value, conventionally last.

What does divide(10, 2) return on success in a func (float64, error)?

  • 5, nil
  • 0, an error
  • nil, 5
  • 5 only

Answer: 5, nil. On success you return the real result and nil for the error.

What is the cornerstone Go error-handling idiom?

  • try { } catch { }
  • throw err
  • assert(err)
  • if err != nil { return err }

Answer: if err != nil { return err }. You check the returned error immediately, before trusting the result.

errors.New("x") builds...

  • a formatted error with data
  • a simple error from a fixed message
  • a panic
  • a sentinel that auto-wraps

Answer: a simple error from a fixed message. errors.New makes a basic error from a constant message; fmt.Errorf adds formatting.

What does fmt.Errorf("user %d not found", id) do?

  • builds an error with the id baked in
  • panics
  • returns nil
  • wraps another error

Answer: builds an error with the id baked in. fmt.Errorf is errors.New with formatting; %d inserts the dynamic value.

Which verb wraps an error so errors.Is keeps working?

  • %v
  • %s
  • %w
  • %d

Answer: %w. %w preserves the chain link; %v flattens to text and loses it.

When is panic appropriate?

  • for bad user input
  • for unrecoverable programmer bugs
  • for a missing file
  • instead of returning errors

Answer: for unrecoverable programmer bugs. Reserve panic for impossible states; expected failures are errors you return.

recover() must be called inside...

  • main only
  • a goroutine
  • an init function
  • a deferred function

Answer: a deferred function. recover only stops a panic when called from within a deferred function.

Why might a function wrongly return nil instead of an error?

  • the error was logged
  • err was shadowed with := inside a block
  • errors.New returns nil
  • panic swallowed it

Answer: err was shadowed with := inside a block. Using := inside an if/block declares a new err; the outer one is never updated.

To discard the result but keep the error, you write...

  • result, _ := f()
  • err, _ := f()
  • _, err := f()
  • _ := f()

Answer: _, err := f(). Assign the result to the blank identifier _ and keep err to check it.