Find Unchecked Errors in Go With the errcheck Linter
Use errcheck to find ignored errors in Go code: what it reports, the -blank and -asserts flags, exclude files, CI use and how golangci-lint fits.

The compiler is strict about unused variables and imports and says nothing at all about an unused error. This compiles and runs, and the failure disappears:
Tested with Go 1.25.6, macOS on Apple Silicon (arm64); errcheck documentation read, tool not executed here
package main
import (
"fmt"
"os"
)
func main() {
os.Remove("/nonexistent/dir/file.txt")
fmt.Println("cleanup done")
}cleanup doneos.Remove returned an error, since the path does not exist, and the program printed cleanup done regardless. In a real service the call might be deleting a lock file, writing a checkpoint or closing a connection, and the program would carry on as though it had worked. The tool that catches this class of mistake is errcheck.
This article covers what it reports, how to run it, and how to tune it. It belongs to the error handling guide and pairs with ignoring errors on purpose. The details of errcheck below come from the project's README; I did not run the tool for this article, so the sample output is described and not pasted. Everything else ran on Go 1.25.6.
What it checks
The README states the rule plainly: for every callable not in the excluded list, all returned errors must be either assigned to a variable or explicitly discarded by assigning them to _. The program above breaks it on the os.Remove line, because the result is dropped. The same program with the discard made explicit passes:
package main
import (
"fmt"
"os"
)
func main() {
// The path may not exist; a missing file is the outcome we want.
_ = os.Remove("/nonexistent/dir/file.txt")
fmt.Println("cleanup done")
}cleanup doneThe behavior is identical, and the second version records a decision. That is the entire philosophy: it does not claim every error needs handling, only that none should be ignored by accident. The README also says what it does not do: it performs no analysis on errors that were assigned. If you write err := foo() and then overwrite err before reading it, errcheck says nothing, and the README points to staticcheck for that kind of check.
Installing and running
Install it with the Go toolchain:
go install github.com/kisielk/errcheck@latestThe README notes that current versions require Go 1.25 or newer. Run it from the module root:
errcheck ./...It loads the packages, finds the unchecked calls and prints one line per finding with the file, line and column and the offending call, so a clean tree prints nothing. Confirm how your version signals findings through its exit status before you wire it into a pipeline. The -abspath flag prints absolute paths, -tags takes build tags if your code uses them, and -mod selects the module download mode.
The flags that matter
-blank makes it stricter: it also reports errors assigned to the blank identifier. With it on, _ = os.Remove(...) is flagged too. That is useful in a codebase that wants every discard justified, and noisy where discards are common. Teams that turn it on usually pair it with a convention that an intentional discard has a comment, and then review the list.
-asserts adds the unchecked type assertion: v := x.(T) without the second ok result, which panics when x is not a T. It is a different mistake from an ignored error and the same shape of silent hazard, so many projects enable both.
-exclude file names a file listing functions to skip. One per line, in the form package.Function for functions and (*package.Type).Method for methods, with // comments allowed. The README's own example excludes io.Copy(*bytes.Buffer) and (*net/http.Client).Do, with the comment that sometimes you do not care whether an HTTP request fails. A line may add a type in parentheses to exclude a call only when the first argument has that type, and the special forms (os.Stdout) and (os.Stderr) match a literal standard stream, which is how printing to the terminal is excluded.
-excludeonly disables the built-in exclude list. By default errcheck combines your file with an internal list of standard library functions that return an error but are documented never to. That list is why writes to a bytes.Buffer or a hash do not clutter the report (ignoring errors on purpose covers which those are). Run with -verbose to see the combined list in effect.
Where it complains too much
Two patterns cause most of the complaints, and both have standard answers.
defer f.Close(). A deferred call whose error is dropped can be reported unless the function is excluded, depending on the version and its built-in list. For a file opened read-only, the error is not actionable, so projects exclude it ((*os.File).Close in the exclude file) and keep the check on for write paths by handling those explicitly (errors from deferred Close calls). Excluding Close globally is a judgment call; a middle path is to leave it on and change write-path closes to the named-result pattern.
fmt.Fprintf to a writer you do not control. Printing to a ResponseWriter or a log can fail and nobody can respond usefully. The README's (os.Stdout) special case handles the terminal, and a project-specific exclude line handles the rest.
Resist the temptation to exclude broadly to get a clean run. Each exclusion is a place where an error will be ignored in future code too, and a narrow rule ("this function, with this argument type") is better than a wide one.
Putting it in CI
Run it as a step next to go vet and the tests. A minimal pipeline job installs it at a pinned version, so a new release does not change results unexpectedly, and runs errcheck ./..., failing the build on any output. Introduce it to an existing codebase in two steps: run it once, fix or explicitly discard every finding in one pass, and then make it a required check. Turning on a linter that reports four hundred findings and asking people to fix them gradually produces a check everyone ignores.
If your project already uses golangci-lint, errcheck is one of its linters, and the same options appear in its configuration file, with the benefit of a single tool, a single cache and a single place to configure exclusions. The trade-off is that you depend on the bundled version. Either way, the check is cheap, and the first run on a mature codebase tends to find a handful of real bugs: a Close on a written file, a Flush that nobody looked at, a Rollback whose failure was swallowed.
What it will not find
Errcheck sees dropped results of calls. It does not see an error logged and then forgotten, an error checked and handled wrongly, or a panic. It will not tell you that you compared a wrapped error with == (Is and As covers that) or that you shadowed err (variable shadowing). For those, go vet, staticcheck and review are the tools. Think of errcheck as the cheap, narrow net that catches the most common silent failure, and nothing more. The project lives at github.com/kisielk/errcheck, and its README is the authority on the flags and the exclude format.


