Errors are values
A Go function that can fail returns an error as its last result. The caller checks it immediately.
Output:
parsed: 42
could not parse: strconv.Atoi: parsing "forty-two": invalid syntax
That is the whole mechanism. There are no exceptions, no try or catch, and no hidden control flow: an error travels only where your code passes it. The cost is visible repetition of if err != nil. The benefit is that every failure point is visible on the page, and you decide at each one what happens.
The error type
error is a built-in interface with one method:
type error interface {
Error() string
}
Any type with an Error() string method is an error. A nil error means success. Printing an error with fmt.Println(err) or %v calls Error().
Creating errors
Two functions cover most cases.
errors.New makes an error with fixed text. fmt.Errorf formats one, with the same verbs as Printf. Error strings by convention start lowercase and have no trailing punctuation, because they are usually embedded in longer messages: load config: open app.yaml: no such file or directory.
The if err != nil pattern
The idiomatic shape is: call, check, return early. The success path stays at the left margin, and each failure exits as soon as it happens.
func loadUser(id int) (*User, error) {
row, err := db.Query(id)
if err != nil {
return nil, err
}
u, err := parseUser(row)
if err != nil {
return nil, err
}
if err := u.Validate(); err != nil {
return nil, err
}
return u, nil
}
Two conventions to notice:
- On error, return the zero value for the other results (
nil,0,""). Callers must not use them whenerr != nil. if err := f(); err != nilscopeserrto theifwhen the function returns only an error. It keeps the outer scope clean.
Avoid the else after an error return. if err != nil { return err } else { ... } just indents the happy path for no reason.
Adding context when you return an error
An error passed up unchanged loses the story of where it came from. open config.yaml: no such file or directory does not tell you which step of startup failed. Add context with fmt.Errorf and the %w verb:
Output:
start server: read config: open /etc/myapp/config.yaml: no such file or directory
true
Each layer adds what it was doing, and the final message reads like a trail from the top of the call down to the cause. Good context names the operation and the input: parse line 12, fetch user 42. Do not add "error" or "failed" at every level; the message is already an error.
%w wraps: it keeps the original error inside the new one, so errors.Is and errors.As can still find it. %v only copies the text. Use %v when you deliberately want to hide an implementation detail from callers, for example so they cannot come to depend on a database driver's error type.
Checking for specific errors: errors.Is and errors.As
Sometimes the caller needs to react to one kind of failure: a missing file means "use defaults", a timeout means "retry". Two functions answer that, and both look through every layer of wrapping.
Rules of thumb:
- Compare to predefined error values (sentinels such as
io.EOF,os.ErrNotExist,sql.ErrNoRows) witherrors.Is, not==.==fails once the error is wrapped. - Extract a typed error with
errors.As, not a type assertion, for the same reason.errors.Astakes a pointer to a variable of the target type. - Never match on
err.Error()text. Messages change between versions, and matching text breaks silently when they do.
Defining your own sentinel errors and error types, and joining several errors with errors.Join, is covered in custom errors.
Handle an error once
An error should be handled exactly once. Handling means one of: returning it (usually wrapped), logging it and continuing, retrying, or turning it into a response for the user. Doing two of these is the most common error bug in Go code.
// Wrong: logged here, and returned, so it is logged again by every caller.
if err != nil {
log.Printf("could not fetch user: %v", err)
return err
}
// Right: add context and return. The top of the program logs once.
if err != nil {
return fmt.Errorf("fetch user %d: %w", id, err)
}
Logging and returning produces the same failure several times in the logs, each with less context than the final message. Let errors flow up to the place that can decide what to do (an HTTP handler, a main, a worker loop), and log there.
Where errors end up
At the top of the program, something has to act on the error. In main that usually means printing it and exiting with a non-zero status:
Running this with no arguments prints error: usage: app <name> to stderr and exits with status 1 (type a name in the Args panel to see the other path). Keeping main to this shape, with the real work in run, makes the program testable and means defer statements inside run still execute, since os.Exit skips deferred calls.
In an HTTP server the top is the handler: it maps the error to a status code and a safe message for the client, and logs the detailed message for you.
Errors you may ignore, and ones you must not
Ignoring an error is sometimes correct, but make it explicit with _ so readers know it was a decision:
_ = conn.SetDeadline(t) // best effort
Some calls cannot fail in practice (strings.Builder.WriteString, bytes.Buffer.Write). Others look harmless and are not: Close on a file you wrote can report that the data never reached the disk, and json.Marshal fails on channels and functions. When in doubt, check.
The errcheck linter (included in golangci-lint) reports unchecked errors. go vet does not flag them on its own.
errors and panics
Go has panic too, but it is not an exception system. Use errors for anything that can go wrong in normal operation: bad input, missing files, network failures. Reserve panic for bugs (an impossible state, a broken invariant) and for startup failures where continuing makes no sense. A library should almost never panic across its API. See panic and recover.
Reducing repetition
if err != nil is verbose, and proposals to add new syntax for it have been declined repeatedly; the Go team announced in 2025 that it is no longer pursuing syntax changes for error handling. Some patterns reduce the noise within the language:
- Return early and keep functions small. Most repetition comes from long functions doing many steps.
- The sticky error. For a sequence of writes, keep the first error in a struct field and make later calls no-ops once it is set.
bufio.Writerworks this way: you check the error once afterFlush. - Wrap once per function. A deferred closure over a named result can add the same context to every error the function returns (see defer).
Common mistakes
- Using a value when err is non-nil. Check first, then use.
- Log and return. Pick one.
- Comparing with
==after wrapping. Useerrors.Is. - Losing the cause with
%v. Use%wunless hiding it is the point. - Returning a typed nil pointer as
error.var e *MyErr; return eis non-nil to the caller. Return a literalnil. - Capitalized or punctuated messages.
errors.New("Failed to connect.")reads badly once wrapped. Writeconnect to db: ....
Frequently Asked Questions
How does error handling work in Go?
Functions that can fail return an error as their last result. The caller checks it right away: v, err := f(); if err != nil { return err }. An error is an ordinary interface value with one method, Error() string, and nil means success. There are no exceptions.
Does Go have try/catch?
No. Go has no exceptions and no try/catch. Expected failures are returned as error values and checked with if err != nil. panic and recover exist, but they are for programming bugs and unrecoverable states, not for normal error flow.
How do I return an error in Go?
Declare error as the last result and return nil on success. Create errors with errors.New("message") for fixed text or fmt.Errorf("reading %s: %w", name, err) to add context to an error you received. On failure, return zero values for the other results.
What is the difference between %w and %v in fmt.Errorf?
Both put the original error's message into the new one. %w also wraps it, so errors.Is and errors.As can still find the original. %v produces a new error with only the text. Use %w when callers may need to check the cause, and %v when you want to hide it.
How do I check which error was returned in Go?
Use errors.Is(err, target) to compare against a sentinel error like io.EOF or os.ErrNotExist, and errors.As(err, &target) to extract a specific error type such as *fs.PathError. Both walk through wrapped errors. Avoid comparing err.Error() strings.