Go doesn’t have exceptions. Functions that can fail return an error as their last value, and the caller decides what to do. The whole story fits in one interface, one package, and a handful of patterns — but interviewers love to test whether you actually understand the idioms.
import "errors"
import "fmt" // for fmt.Errorf wrapping
At a glance
| Need to… | Use |
|---|---|
| Create a simple error | errors.New("message") |
| Format a message with values | fmt.Errorf("...%v...", x) |
| Wrap another error with context | fmt.Errorf("...%w...", err) |
| Check if an error is a specific sentinel | errors.Is(err, ErrNotFound) |
| Extract a specific error type | errors.As(err, &myErr) |
| Combine multiple errors | errors.Join(err1, err2) |
The error type
error is just a built-in interface with one method:
type error interface {
Error() string
}
Anything with an Error() string method satisfies it. The convention is to return errors as the last return value:
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("divide by zero")
}
return a / b, nil
}
result, err := divide(10, 0)
if err != nil {
fmt.Println("error:", err)
return
}
if err != nilis the most-typed line of code in Go. It’s verbose. Get used to it.
Creating errors
errors.New — a constant message
err := errors.New("file not found")
fmt.Println(err) // "file not found"
Use when the message doesn’t need any dynamic values.
fmt.Errorf — a formatted message
err := fmt.Errorf("file %q not found", filename)
Use when you want to include variables. Same format verbs as fmt.Printf.
Sentinel errors — named values you can compare against
A sentinel is a package-level error you export so callers can check for it:
package store
import "errors"
var ErrNotFound = errors.New("not found")
func Get(id string) (Item, error) {
if !exists(id) {
return Item{}, ErrNotFound
}
// ...
}
Callers compare:
item, err := store.Get("xyz")
if errors.Is(err, store.ErrNotFound) {
// handle missing item
}
Don’t compare with
==when there might be wrapping involved (see next section). Useerrors.Is.
Familiar examples from the standard library: io.EOF, sql.ErrNoRows, os.ErrNotExist.
Wrapping errors with %w
When a function passes an error up, it usually wants to add context — what it was doing when the error happened. Wrapping preserves the original error while adding a layer.
func loadConfig(path string) error {
data, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("loadConfig %q: %w", path, err)
}
// ...
}
The %w verb is special — it doesn’t just format the error as text, it wraps it so the caller can still recognize the underlying error.
Result when printed:
loadConfig "config.json": open config.json: no such file or directory
You see the wrapping chain in order: outer context → inner context → original error.
Use
%wexactly once perfmt.Errorfcall. Multiple%wworks since Go 1.20 (the result wraps all of them), but stick to one unless you’re deliberately joining errors.
Checking errors — errors.Is and errors.As
These are the two functions that make wrapping useful.
errors.Is — “is this error (or anything it wraps) equal to X?”
err := loadConfig("missing.json")
if errors.Is(err, os.ErrNotExist) {
fmt.Println("config file is missing")
}
Is walks the wrapping chain, comparing each layer to os.ErrNotExist. If any of them match, it returns true. This is what makes sentinels work even after wrapping.
errors.As — “is there an error of this type in the chain?”
When you have a custom error type with extra fields, As extracts it:
type ValidationError struct {
Field string
Msg string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("%s: %s", e.Field, e.Msg)
}
// ...
err := saveUser(u)
var vErr *ValidationError
if errors.As(err, &vErr) {
fmt.Println("validation failed on field:", vErr.Field)
}
As walks the chain, finds the first error of matching type, and assigns it to the target pointer.
errors.Isis for sentinel values.errors.Asis for custom types. That’s the one-line rule.
Custom error types
When an error needs to carry extra information (a status code, a field name, the input that failed), make a type.
type HTTPError struct {
Code int
Msg string
}
func (e *HTTPError) Error() string {
return fmt.Sprintf("HTTP %d: %s", e.Code, e.Msg)
}
// Use it:
return &HTTPError{Code: 404, Msg: "not found"}
// Caller extracts it:
var httpErr *HTTPError
if errors.As(err, &httpErr) {
fmt.Println("status code:", httpErr.Code)
}
Define the method on the pointer (
*HTTPError), not the value. Then return&HTTPError{...}. This is the standard pattern; it letserrors.Aswork cleanly and avoids accidental copies.
Optional: implement Unwrap for deeper wrapping
If your custom error itself wraps another, expose it:
type DatabaseError struct {
Query string
Err error
}
func (e *DatabaseError) Error() string {
return fmt.Sprintf("query %q failed: %v", e.Query, e.Err)
}
func (e *DatabaseError) Unwrap() error {
return e.Err
}
Now errors.Is and errors.As can walk through your custom error into whatever it wraps.
Combining errors — errors.Join (Go 1.20+)
When several things go wrong and you want to report all of them:
var errs []error
for _, item := range items {
if err := validate(item); err != nil {
errs = append(errs, err)
}
}
if len(errs) > 0 {
return errors.Join(errs...)
}
The joined error’s message is each child error on its own line. errors.Is and errors.As still work — they check every joined error.
Common interview patterns
”What’s idiomatic Go error handling?”
The expected answer:
- Functions return
(result, error)as the last value. - Caller checks
if err != nil { ... }and decides what to do. - Wrap with
fmt.Errorf("doing X: %w", err)to add context as the error travels up. - At the top of the stack, check with
errors.Is(for sentinels) orerrors.As(for typed errors) to handle specific cases differently from generic ones. - No exceptions, no
try/catch, no hidden control flow.
”Why wrap errors instead of returning them as-is?”
Without wrapping, you lose context: "no such file or directory" doesn’t tell you which file. Wrapping adds the where/what/why as the error climbs the call stack. errors.Is/As still see through the wrapping, so you don’t trade context for the ability to compare.
Return early on error (the “happy path” pattern)
func process(path string) error {
data, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("read %q: %w", path, err)
}
parsed, err := parse(data)
if err != nil {
return fmt.Errorf("parse %q: %w", path, err)
}
if err := validate(parsed); err != nil {
return fmt.Errorf("validate %q: %w", path, err)
}
return save(parsed)
}
Each error is returned immediately and wrapped with the operation it was doing. The “happy path” stays at the left margin, errors short-circuit out. This is the pattern.
Convert a sentinel-or-fail check
func getUser(id string) (*User, error) {
u, err := db.Find(id)
switch {
case errors.Is(err, sql.ErrNoRows):
return nil, ErrUserNotFound // map to your package's sentinel
case err != nil:
return nil, fmt.Errorf("db lookup: %w", err)
}
return u, nil
}
Don’t lose the original error
// ❌ Bad — original error is gone
return fmt.Errorf("something failed: %s", err.Error())
// ❌ Bad — same problem, just uses %v
return fmt.Errorf("something failed: %v", err)
// ✅ Good — wraps so errors.Is/As still work
return fmt.Errorf("something failed: %w", err)
%s and %v format the error as a string. %w preserves the chain.
Quick rules of thumb
- Last return value is
error. Always. - Always check
if err != nil. Don’t ignore errors with_. - Add context with
%w— never%vor%sfor wrapping. errors.Isfor sentinels,errors.Asfor custom types.- Define custom error methods on the pointer (
func (e *MyErr) Error() string). - Sentinels start with
Errby convention:ErrNotFound,ErrTimeout.
Common mistakes
- Ignoring errors with
_— silently swallows real problems. If you really don’t care, comment why. - Wrapping with
%sor%v— flattens the chain;errors.Is/Asstop working. - Comparing wrapped errors with
==— fails when there’s wrapping. Useerrors.Is. errors.As(err, &SomeStruct{})— must pass a pointer to a pointer (or pointer to interface). Pass&myErrwheremyErris*SomeStruct, notSomeStruct{}.- Panicking instead of returning errors —
panicis for programmer mistakes (nil deref, out-of-bounds), not for runtime conditions. Return an error. - Defining error variables without the
Errprefix — readers expectErrNotFound, notNotFoundErrorornotFound.