Skip to content
gohacks.dev
Basics

Panic or Error in Go and When Panic Is the Right Call

Choose between panic and error in Go with a test you can apply: programmer mistakes and impossible states panic, runtime conditions return an error.

6 min read
Summarize in AI
Flat illustration of a log with a failing line and a wrench.

Go gives you two ways for a function to say that something went wrong: return an error, or panic. Every language with both ends up with the same argument about which to use when, and Go's answer is more definite than most. Errors are the normal mechanism, and panics are for the cases where continuing makes no sense. The line between them is easy to state and takes a little practice to apply, because the same symptom, a bad value, can be either, depending on who produced it.

Tested with Go 1.25.6, macOS on Apple Silicon (arm64)

This article gives the test, shows what the standard library does on each side of it, and covers the boundary cases. It belongs to the error handling guide, and recover done right covers the other half of the mechanism. Everything ran on Go 1.25.6.

What each one means

An error says: something did not work, and here is why. The caller is expected to look at it and decide. File not found, request timed out, input did not parse, the user does not exist. None of these is a mistake in the program. They are facts about the world, and a correct program has to be ready for them.

A panic says: the program is in a state it should not be in. An index outside a slice, a nil map write, an invariant that someone promised and broke. The caller cannot usefully handle it, because the cause is in the code, and the fix is to change the code.

The practical test is this: could a correct program, given correct inputs, still hit this? If yes, it is an error. If the only way to reach it is for someone to have written a bug, it is a panic.

The standard library draws the line

The library applies the test consistently, and its examples are the best guide. Here are several panics from calls that a correct caller would never make, each caught and printed so you can see the message:

main.goGo
package main

import (
	"fmt"
	"regexp"
	"strings"
	"sync"
	"time"
)

func try(name string, f func()) {
	defer func() { fmt.Printf("%-16s %v\n", name, recover()) }()
	f()
}

func main() {
	try("strings.Repeat", func() { strings.Repeat("x", -1) })
	try("time.NewTicker", func() { time.NewTicker(0) })
	try("regexp.Must", func() { regexp.MustCompile("(") })
	try("WaitGroup", func() {
		var wg sync.WaitGroup
		wg.Add(-1)
	})
	try("close nil chan", func() {
		var c chan int
		close(c)
	})
}
Text
strings.Repeat   strings: negative Repeat count
time.NewTicker   non-positive interval for NewTicker
regexp.Must      regexp: Compile(`(`): error parsing regexp: missing closing ): `(`
WaitGroup        sync: negative WaitGroup counter
close nil chan   close of nil channel

Each of these is a call whose arguments the caller controls and could have checked: a negative repeat count, a ticker that fires every zero seconds, a counter driven below zero. The functions have no useful way to continue, and returning an error would only invite the caller to ignore it and carry on in a broken state. regexp.MustCompile is the same idea one step removed: it exists for patterns written as constants in source code, where a malformed pattern is a typo to be fixed before the program ships. The same package has regexp.Compile, which returns an error, for patterns that come from users or files, where a bad pattern is an expected condition.

The pair is the model. When the same operation can fail for a reason in the program or for a reason in the world, provide both: the panicking form for what you control and the error-returning form for what you do not.

Must functions

The convention for the panicking form is the Must prefix: regexp.MustCompile, template.Must, Must helpers in many packages. It wraps a call that returns an error and panics if there is one. Its home is initialization:

main.goGo
package main

import (
	"fmt"
	"os"
	"regexp"
	"text/template"
)

var (
	idPattern = regexp.MustCompile(`^[a-z]{3}-\d{4}$`)
	greeting  = template.Must(template.New("g").Parse("Hello, {{.}}\n"))
)

func main() {
	fmt.Println(idPattern.MatchString("abc-1234"), idPattern.MatchString("ABC-1"))
	greeting.Execute(os.Stdout, "world")
}
Text
true false
Hello, world

These two variables are built when the package loads. If the pattern or the template were wrong, the program would fail on the first run, every run, which is exactly when you want to learn it. Moving the failure to startup is the benefit. Writing Must around a call whose input comes at run time defeats the purpose, because now bad input crashes the server (the Must convention says where they belong).

Where programs go wrong

Three mistakes account for most misuse.

Panicking on expected failure. A function named ReadConfig that panics when the file is missing makes every caller write defer recover(), or accept a crash for something routine. If an operation touches the file system, the network, a user's input or another process, it returns an error.

Returning an error for a bug. The opposite mistake is quieter. A function that returns an error for a nil receiver, or for an index it was given that is out of range, hides a programming mistake in a value that the caller will probably log and ignore. When the state can only arise from a bug, a panic makes the bug loud at the line that caused it. A nil map write panics for this reason, and the runtime's own panics follow it (every runtime panic).

Using panic for control flow. Throwing a panic to unwind out of deep recursion and catching it at the top is possible, and the standard library does it in a few places: encoding/json and text/template use panics internally for speed and convert them into errors before returning. That is an implementation detail behind a clean boundary. Doing it across your own public API forces callers to understand it.

Converting at a boundary

Some code must not die from a panic: a server handling a thousand independent requests, a worker pool, a plugin host. The pattern is a recover at the entry of each unit of work that converts a panic into whatever that layer uses for failure:

main.goGo
package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"
)

func safe(next http.HandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		defer func() {
			if rec := recover(); rec != nil {
				http.Error(w, "internal error", http.StatusInternalServerError)
				fmt.Println("recovered:", rec)
			}
		}()
		next(w, r)
	}
}

func main() {
	h := safe(func(w http.ResponseWriter, r *http.Request) {
		var users map[string]string
		users["ada"] = "admin"
	})
	rec := httptest.NewRecorder()
	h(rec, httptest.NewRequest("GET", "/", nil))
	fmt.Println(rec.Code)
}
Text
recovered: assignment to entry in nil map
500

The handler has a bug, the panic is caught at the edge, the client gets a 500, and the process lives. The real net/http server does this for you on each request: it recovers, logs the panic with its stack and closes the connection, so a custom middleware is only needed when you want your own response or logging. This is the only widely accepted use of recover. Anywhere else, ask what bug it is covering up (recover and re-panicking go through the cases).

A short checklist

Before writing panic(...) in your own code:

  • Is the cause a programmer mistake rather than a runtime condition? If not, return an error.
  • Could the caller have avoided it by checking something first? If so, a panic with a clear message is acceptable, and document it in the function's comment.
  • Is this initialization code, where failing to start is the right outcome? Use a Must helper.
  • Will a caller more reasonably want to handle it? Then return an error, even if it is rare.

And before writing recover(): is this the top of a unit of work that must survive? If not, delete it. For the language's own statement of the convention, see the Go blog's Defer, Panic, and Recover, which describes panics as a way to stop the ordinary flow of control, and Effective Go on panic, which gives the guidance on library use.

Share

Keep reading

Get new articles by email

We send one email when a new article is published. No spam.

Unsubscribe in one click. No spam, ever.