errortype
is a Go static analysis tool (linter) that helps prevent subtle bugs in error handling and other areas. It
performs two main checks:
-
Inconsistent Error Type Usage: It analyzes function return values, type assertions, and calls to functions like
errors.As
to ensure that custom error types are used consistently as either pointers or values. -
Pointless Comparisons: It detects comparisons against the address of a newly created value (like
errors.Is(err, &url.Error{})
), which are almost always incorrect.
Choose one of the following installation methods:
brew install fillmore-labs/tap/errortype
go install fillmore-labs.com/errortype@latest
Install eget
, then
eget fillmore-labs/errortype
To analyze your entire project, run:
errortype ./...
Usage: errortype [-flag] [package]
errortype
supports the following flags:
- -overrides
<filename>
: Read type overrides from the specified YAML file, see the “Override File” section for more details. - -suggest
<filename>
: Append suggestions to an override file for review. Use-
for standard output. - -check-is: Suppress diagnostics on
errors.Is
if the compared type has anIs(error) bool
method (default: true). - -deep-is-check: (Experimental) When checking an
Is(error) bool
method, diagnose any call to an unwrapping function (likeerrors.Is
orerrors.As
). By default, only calls that use thetarget
parameter are flagged (default: false). - -style-check: Check for confusing uses of
errors.As
(default: true). - -unchecked-assert: Diagnose unchecked type asserts on errors (default: false).
- -c
<N>
: Display N lines of context around each issue (default: -1 for no context, 0 for only the offending line). - -test: Analyze test files in addition to source files (default: true).
- -heuristics: (Experimental) List of heuristics used (default: “usage,receivers”, “off” to disable).
- -trace: (Experimental) Output information for result tracing.
One of the most common and subtle error-handling bugs in Go occurs when error types are used inconsistently — sometimes as values and sometimes as pointers. This inconsistency can lead to subtle, hard-to-find bugs.
Consider the following code, which attempts to provide a more specific error message for an incorrect AES key size (Go Playground):
package main
import (
"crypto/aes"
"errors"
"fmt"
)
func main() {
key := []byte("My kung fu is better than yours")
_, err := aes.NewCipher(key)
var kse *aes.KeySizeError
if errors.As(err, &kse) {
fmt.Printf("AES keys must be 16, 24, or 32 bytes long, got %d bytes.\n", kse)
} else if err != nil {
fmt.Println(err)
}
}
This code doesn't work as intended because aes.KeySizeError
is designed to be used as a value, not a pointer. As
written, the code prints the generic error message instead of the custom one.
Changing line 13 to var kse aes.KeySizeError
fixes the issue, and the program correctly prints “AES keys must be 16,
24, or 32 bytes long, got 31 bytes.”
The errortype
linter prevents these issues by automatically detecting the intended usage of error types and reporting
inconsistencies in:
- Function return values
- Type assertions and type switches
- Calls to
errors.As
and similar functions (e.g., fromtestify
)
In the above example, errortype .
would report:
.../main.go:14:20: Target for value error "crypto/aes.KeySizeError" ⏎
is a pointer-to-pointer, use a pointer to a value instead: ⏎
"var kse aes.KeySizeError; ... errors.As(err, &kse)". (et:err)
The message suggests changing the variable declaration to var kse aes.KeySizeError
, which corrects the bug.
The linter determines an error type's intended use (pointer vs. value) by analyzing the package where the error is defined. It uses the following order of precedence:
-
Overrides (the highest priority): User-defined overrides (see below) are applied, overriding any detected usage.
-
Package-Level Variable Assignments: If present,
var _ error = ...
assignments are used as explicit declarations of intent.var _ error = ValueError{} // Determines ValueError is a "value" type. var _ error = (*PointerError)(nil) // Determines PointerError is a "pointer" type.
-
Usage Within Functions: If still undecided, the linter analyzes usage within top-level functions (in
return
statements or type assertions). Consistent usage can determine the type.return ValueError{} // Suggests a value type if _, ok := err.(*PointerError); ok { /* ... */ } // Suggests a pointer type
Note: This heuristic is a fallback and should not be relied upon for defining a type's contract.
-
Consistent Method Receivers (lowest priority): As a final heuristic, if all methods on a type have a consistent receiver (all-value or all-pointer), that style is used.
To make an error type's intended usage explicit and ensure errortype
can automatically determine it, add a
package-level variable assignment anywhere in the package where the error is defined:
type ValueError struct{ /* ... */ }
func (v ValueError) Error() string { /* ... */ }
type PointerError struct{ /* ... */ }
func (p PointerError) Error() string { /* ... */ }
// In your package, explicitly declare the intended usage.
var (
_ error = ValueError{}
_ error = (*PointerError)(nil)
)
When the linter reports types from an imported package that have ambiguous or inconsistent usage, you can guide the linter in two ways:
-
Local Overrides: For a one-off fix within a single package, add a
var
block to a source file in that package. This overrides the detected usage for that type within this package only.// In your code, force a specific usage for an imported type. var _ error = imported.ValueError{} var _ error = (*imported.PointerError)(nil)
-
Global Override File: For project-wide overrides, use an
errortypes.yaml
file, see “Override File”.
Beyond error handling inconsistencies, errortype
also detects a second class of subtle bugs: comparisons against the
address of newly created values. These comparisons, such as ptr == &MyStruct{}
or ptr == new(MyStruct)
, are almost
always incorrect and can lead to unexpected behavior.
According to the Go language specification, taking the address of a composite
literal (&MyStruct{}
) or calling new()
creates a new allocation:
“Calling the built-in function
new
or taking the address of a composite literal allocates storage for a variable at run time.”
Each allocation gets a unique address:
“Taking the address of a composite literal generates a pointer to a unique variable initialized with the literal's value.”
This means ptr == &MyStruct{}
will almost always evaluate to false
, regardless of what ptr
points to. The only
exception is zero-sized types, where the behavior is undefined:
“Pointers to distinct zero-size variables may or may not be equal.”
Here are examples that errortype
will flag:
import (
"errors"
"log"
"net/url"
)
func handleNetworkError(err error) {
// This will always be false - &url.Error{} creates a unique address.
if errors.Is(err, &url.Error{}) {
log.Fatal("Cannot connect to service")
}
// Correct approach:
var urlErr *url.Error
if errors.As(err, &urlErr) {
log.Fatal("Error connecting to service:", urlErr)
}
// ...
}
import (
"github.com/operator-framework/api/pkg/operators/v1alpha1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"time"
)
// Checking if an operator update strategy matches expected values
func validateUpdateStrategy(spec *v1alpha1.CatalogSourceSpec) {
expectedDuration := 30 * time.Second
// This comparison will always be false - &metav1.Duration{} creates a unique address.
if spec.UpdateStrategy.Interval != &metav1.Duration{Duration: expectedDuration} {
// ...
}
// Correct approach: Dereference the pointer and compare values (after a nil check).
if spec.UpdateStrategy.Interval == nil || spec.UpdateStrategy.Interval.Duration != expectedDuration {
// ...
}
}
errortype
includes special handling for errors.Is
and similar functions to reduce false positives. The linter
suppresses diagnostics when:
-
The error type has an
Unwrap() error
method, aserrors.Is
traverses the error tree. -
The error type has an
Is(error) bool
method, as custom comparison logic is executed.
This behavior can be disabled with the -check-is=false
flag.
For more complex projects or when working with third-party libraries that have ambiguous or wrongly detected error type usage, you may need to provide explicit guidance to the linter through an override file.
You can generate a sample override file with the -suggest
flag. This file will contain a list of types that require a
decision:
errortype -suggest=errortypes.yaml ./...
This command creates (or appends to) errortypes.yaml
with the following structure:
# Override types for your.path/package
---
pointer: # Types that should always be used as pointers
- imported.path/one.PointerOverride
value: # Types that should always be used as values
- imported.path/two.ValueOverride
suppress: # Types to completely ignore during analysis
- imported.path/one.ErrorToIgnore
inconsistent: # Types that are used inconsistently (generated by -suggest)
- imported.path/two.InconsistentUsage
The inconsistent
section is only generated by -suggest
and is ignored by the linter. You should review all entries
and move them to the pointer
, value
, or suppress
sections as appropriate.
Once your errortypes.yaml
file is configured, use it with the -overrides
flag:
errortype -overrides=errortypes.yaml ./...
This instructs the linter to use your specified configuration, resolving ambiguities and suppressing noise from types you wish to ignore.
Note: Always review suggestions before adding them to your override file. A suggestion makes your code consistent with how the type is used in your package, but this may conflict with how the type was designed to be used in its defining package. When possible, fixing the inconsistency by refactoring the code is preferable to forcing an override.
It's important to understand the difference between autodetection and overrides.
-
Autodetection runs on the package where an error type is defined, see “How Intended Usage is Detected”. This is the ideal place to establish the intended usage.
-
Overrides are based on the usage within your code. They force a specific pointer or value style, overriding what was detected in the defining package.
Every suggestion should be reviewed before being used as an override. An inconsistent
usage report may indicate a
genuine opportunity to refactor and improve your error handling. When possible, it is always better to improve detection
in the defining package by making the usage explicit, see
“Designing Linter-Friendly Packages”.
The linter currently does not look into expression switch
logic, like
switch err {
case io.EOF:
return true
}
A survey of more than 750 popular open source projects showed that there are nearly no issues in usages.
errortype
uses short diagnostic codes to categorize the issues it finds, making it easier to understand and address
specific problems.
-
et:ret
(Return Mismatch): An error type is returned incorrectly (returning a value error as a pointer or vice versa). -
et:ast
(Assertion Mismatch): An error type is used incorrectly in a type assertion or type switch. -
et:err
(Argument Mismatch): An error type is passed incorrectly as a target to anerrors.As
-like function. -
et:emb
(Embedded/Ambiguous Usage): The linter could not determine if an error is a pointer or value type. This can be resolved with an override.
-
et:cmp
(Pointless Error Comparison): A pointer is compared against the address of a newly created value inerrors.Is
or similar functions. This comparison is almost alwaysfalse
, or its result is undefined for zero-sized types.if errors.Is(err, &url.Error{}) { // This comparison will always be false // ... }
Fix: Use
errors.As
to check if an error is of a certain type. If you need to check for a specific sentinel error instance, define it as a package-level variable and compare against that.var urlErr *url.Error if errors.As(err, &urlErr) { // Correct approach // ... }
-
et:equ
(Pointless Comparison): A pointer is compared against the address of a newly created value using equality operators. This comparison is almost alwaysfalse
, or its result is undefined for zero-sized types.if ptr == &MyStruct{} { // This comparison will always be false // ... }
Fix: Dereference the pointer and compare the underlying values (
*ptr == MyStruct{}
). If you need to check for a specific sentinel instance, define it as a package-level variable and compare against that.
-
et:unw
(Calling Unwrap): An unwrapping function (likeerrors.Is
) was found inside anIs(error) bool
method.func (MyEOFError) Is(err error) bool { return errors.Is(err, io.ErrUnexpectedEOF) }
“An
Is
method should only shallowly compareerr
and thetarget
and not callUnwrap
on either.”Fix: Replace the call with a direct, shallow comparison, such as
err == io.ErrUnexpectedEOF
.func (MyEOFError) Is(err error) bool { return err == io.ErrUnexpectedEOF }
-
et:sty
(Style Mismatch): The target argument to anerrors.As
-like function is not an address operation on a variable.// Confusing: checks for a value, but looks like a pointer check. if errors.As(err, &MyError{}) { /* ... */ }
While this is valid code, it can be misleading. The expression
&MyError{}
creates a pointer to a struct literal. When passed toerrors.As
, it checks iferr
wraps aMyError
value, not a pointer. This is easily misread, obfuscating the check's true intent.Fix: To improve clarity, declare a variable of the target error type and pass its address to
errors.As
.var e MyError // Or "var e *MyError" for pointer errors if errors.As(err, &e) { /* ... */ } // The clear, recommended style
This longer form is unambiguous and clearly states the type being checked for.
-
et:arg
(Invalid Argument): The target argument to anerrors.As
-like function is invalid (not a pointer to a type implementingerror
). This is also flagged by the standard Goerrorsas
linter. -
et:auc
(Unchecked Type Assert): An unchecked type assert might lead to a run-time panic on a wrapped error.if err.(*net.AddrError).Err == "missing port in address" { /* ... */ }
Fix: While sometimes valid, prefer
errors.As
.
Add a file .custom-gcl.yaml
to your source with
---
version: v2.4.0
name: golangci-lint
destination: .
plugins:
- module: fillmore-labs.com/errortype
import: fillmore-labs.com/errortype/gclplugin
version: v0.0.5
then run golangci-lint custom
from your project root. You get a custom golangci-lint
executable that can be
configured in .golangci.yaml
:
---
version: "2"
linters:
enable:
- errortype
settings:
custom:
errortype:
type: module
description: "errortype helps prevent subtle bugs in error handling."
original-url: "https://fillmore-labs.com/errortype"
settings:
overrides:
pointer:
- test/a.PointerOverride
value:
- test/a.ValueOverride
suppress:
- test/a.SuppressOverride
style-check: true
deep-is-check: false
check-is: true
unchecked-assert: false
and can be used like golangci-lint
:
./golangci-lint run .
See also the golangci-lint module plugin system documentation.
- Background on the problem this linter solves
- Why you shouldn't call
Unwrap
inIs(error) bool
methods - Enhanced error inspection with better ergonomics
- Proposal for a generic
errors.As
variant
This project is licensed under the Apache License 2.0. See the LICENSE file for details.