Common Go Error Handling Mistakes and Their Messages

The error handling mistakes Go beginners make most, with the exact compiler and runtime messages they produce and how to fix each one.

Most error handling bugs in Go fall into a small set of patterns. Some are caught by the compiler with a message that is confusing the first time you see it. Others compile fine and only show up as wrong behavior or a crash. This page lists each mistake with the exact message you will see, so you can search for it and fix it quickly.

assignment mismatch: 1 variable but ... returns 2 values

./main.go:9:7: assignment mismatch: 1 variable but strconv.Atoi returns 2 values

The function returns a result and an error, but you only gave one variable:

n := strconv.Atoi("42") // does not compile

Go makes you receive the error. The fix is to accept both values and check the error:

n, err := strconv.Atoi("42")
if err != nil {
	return err
}

declared and not used: err

./main.go:10:5: declared and not used: err

You received err but never looked at it. Go refuses to compile unused local variables, which in this case protects you from silently ignoring a failure. Add the check. If you truly do not care about the error, replace err with _, but only with a reason you could explain to a reviewer.

not enough return values

./main.go:8:10: not enough return values
	have (error)
	want (int, error)

The function returns (int, error) but your return only gave the error. Every return must supply all results. Put a zero value in front:

return 0, err

The opposite mistake, return n in a function that also returns error, gives the same message with have (int). Add , nil for the success case.

missing return

./main.go:12:1: missing return

At least one path through the function reaches the closing brace without a return. This often happens after an if err != nil { return ... } block when the success return was forgotten. Add the final return value, nil.

no new variables on left side of :=

./main.go:11:9: no new variables on left side of :=

Every variable on the left of := already exists. This shows up when you write x, err := f() and then later x, err := g() with the same names. Use = for the second one, or give the result a different name.

E does not implement error (method Error has pointer receiver)

cannot use E{} (value of struct type E) as error value in variable declaration: E does not implement error (method Error has pointer receiver)

Your Error method is defined on *E, but you returned E{}. Return a pointer instead:

return &E{...}

Or define the method on the value receiver, func (e E) Error() string. Keep one form per type, and make errors.As targets match it.

Checking the Error After Using the Result

This compiles, then crashes when the request fails:

resp, err := http.Get(url)
defer resp.Body.Close() // resp is nil if err != nil
if err != nil {
	return err
}
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x40 pc=0x...]

Always check err before touching the other results. Move the defer below the if.

The most common crash in beginner Go code:

Whenever you see nil pointer dereference, look at the lines just above the crash for a result that was used before its error was checked.

Logging an Error and Also Returning It

if err != nil {
	log.Printf("query failed: %v", err)
	return err
}

This compiles and seems careful, but each layer that does it logs the same failure again. One problem turns into five log lines, often with different timestamps, and it becomes hard to tell how many failures really happened.

Handle each error once: either return it with context, or log it and stop. Usually only the top of the program, such as main or the HTTP handler, logs.

if err != nil {
	return fmt.Errorf("query orders for user %d: %w", userID, err)
}

Using %v Instead of %w

return fmt.Errorf("read config: %v", err)

The message looks right, but errors.Is(err, fs.ErrNotExist) above this point now returns false. Use %w whenever you pass an error on, unless you deliberately want to hide the cause.

Comparing Errors With == or Their Text

if err == sql.ErrNoRows { ... }                     // fails once wrapped
if err.Error() == "sql: no rows in result set" { }  // fails when text changes

Both checks work until someone adds context with %w. Use errors.Is(err, sql.ErrNoRows).

Returning a nil Pointer as an error

var e *MyError
return e // err != nil is true for the caller

Return a literal nil on success. An interface holding a typed nil pointer is not nil.

Ignoring the Error From Close on a Written File

f, _ := os.Create(path)
defer f.Close()
f.Write(data)

There are two problems here: the os.Create error is ignored, and so is Close. When writing, data may only reach the disk when the file is closed, so a full disk can show up as a Close error. Ignoring it means your program reports success while the file is empty or cut short.

For files you only read, ignoring the Close error is acceptable. For files you write, check it, for example with a named result and a deferred errors.Join(err, f.Close()).

Unchecked errors compile without warnings:

The compiler only complains about unused variables, not about errors dropped with _ or calls whose result is ignored, such as f.Write(data) on its own line. Linters like errcheck (included in golangci-lint) report these.

Shadowing err Inside a Block

var err error
if useCache {
	data, err := readCache() // new err, hides the outer one
	_ = data
}
if err != nil { // always nil here
	return err
}

The inner := created a second err that only lives inside the if. Declare data beforehand and use =:

var data []byte
var err error
if useCache {
	data, err = readCache()
}

A clean error-handling checklist:

Errors are checked before results are used, each error is either returned with %w or handled once, checks use errors.Is and errors.As, success returns a literal nil, and errcheck reports nothing. Code that passes this checklist avoids every mistake on this page.

Summary

The compiler catches missing error variables, unused err, wrong numbers of return values and pointer receiver mismatches, each with a specific message you can search. The harder bugs compile fine: using results before checking errors, logging and returning, %v instead of %w, == on wrapped errors, typed nil returns, unchecked Close on written files and shadowed err variables. Knowing these patterns by sight, and running go vet and errcheck, removes most of them before they reach production.