errortype is a static analysis tool that performs two checks:
-
Inconsistent Error Type Usage: Ensures error types are used consistently as either pointers or values in returns, type assertions, and
errors.Ascalls. -
Pointless Comparisons: Detects comparisons against newly allocated addresses (like
errors.Is(err, &url.Error{})orptr == &MyStruct{}), which are almost always incorrect.
go install fillmore-labs.com/errortype@latestbrew install fillmore-labs/tap/errortypeInstall eget, then:
eget fillmore-labs/errortypeAnalyze your entire project:
errortype ./...| Flag | Description | Default |
|---|---|---|
-overrides <file> |
Read type overrides from a YAML file (see Override File) | — |
-suggest <file> |
Append suggestions to an override file (- for stdout) |
— |
-check-is |
Suppress diagnostics on errors.Is if the type has an Is(error) bool method |
true |
-deep-is-check |
In Is methods, diagnose any unwrapping call, not just those using target |
false |
-style-check |
Check for confusing uses of errors.As |
true |
-unchecked-assert |
Diagnose unchecked type asserts on errors | false |
-c <N> |
Lines of context around each issue (-1 = none, 0 = offending line only) |
-1 |
-test |
Analyze test files | true |
-heuristics <list> |
Heuristics to use (“off” to disable) (Experimental) |
var,usage,receivers |
-tracetypes <regex> |
Trace type detection in matching packages (Experimental) | — |
A common and subtle bug occurs when error types are used inconsistently — sometimes as values, sometimes as pointers.
This can cause errors.As checks to silently fail.
Consider this code (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 prints the generic error because aes.KeySizeError is a value type, not a pointer. Changing line 13 to
var kse aes.KeySizeError fixes it.
Running errortype . reports:
.../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 linter determines an error type's intended use (pointer vs. value) by analyzing its defining package, in order of precedence:
-
Overrides: User-defined overrides (see Override File) take highest priority.
-
Unwraprelated methods: Methods likeIs,As, andUnwrapwith pointer receivers are only visible when the error is used as a pointer.func (e *PointerError) Unwrap() error { /* ... */ } // Only visible from error(&PointerError{}).
-
Package-level variable assignments:
var _ error = ...declarations explicitly state intent.var _ error = ValueError{} // Declares ValueError as a value type. var _ error = (*PointerError)(nil) // Declares PointerError as a pointer type.
-
Usage in functions: Consistent usage in
returnstatements or type assertions.return ValueError{} // Suggests value type if _, ok := err.(*PointerError); ok { /* ... */ } // Suggests pointer type
[!NOTE]
This heuristic is a fallback and should not be relied upon for defining a type's contract.
-
Consistent method receivers: If all methods have the same receiver type, that style is used.
To make intent explicit, add a variable assignment in the declaring package:
type ValueError struct{ /* ... */ }
func (v ValueError) Error() string { /* ... */ }
type PointerError struct{ /* ... */ }
func (p PointerError) Error() string { /* ... */ }
// Explicitly declare intended usage.
var (
_ error = ValueError{}
_ error = (*PointerError)(nil)
)When the linter reports ambiguous usage from an imported package you cannot modify, use an override file (see Override File).
errortype also detects comparisons against newly allocated addresses. Per the
Go spec, &MyStruct{} and new(T) each create a unique address, so
ptr == &MyStruct{} is almost always false. For zero-sized types, the result is undefined.
import (
"errors"
"log"
"net/url"
)
func handleNetworkError(err error) {
// Always 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"
)
func validateUpdateStrategy(spec *v1alpha1.CatalogSourceSpec) {
expectedDuration := 30 * time.Second
// Always false — &metav1.Duration{} creates a unique address.
if spec.UpdateStrategy.Interval != &metav1.Duration{Duration: expectedDuration} {
// ...
}
// Correct: compare values after nil check.
if spec.UpdateStrategy.Interval == nil || spec.UpdateStrategy.Interval.Duration != expectedDuration {
// ...
}
}The linter suppresses diagnostics when the error type has an Unwrap() error method (since errors.Is traverses the
chain) or an Is(error) bool method (custom comparison logic). Disable with -check-is=false.
For complex projects or third-party libraries with ambiguous error types, provide an override file.
Generate a sample with:
errortype -suggest=errortypes.yaml ./...This creates a file 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 ignore during analysis
- imported.path/one.ErrorToIgnore
inconsistent: # Types with inconsistent usage (generated by -suggest, ignored by linter)
- imported.path/two.InconsistentUsageReview entries in inconsistent and move them to pointer, value, or suppress as appropriate. Then run:
errortype -overrides=errortypes.yaml ./...Note
A suggestion makes your code consistent with how the type is used in your package, but this may conflict with its intended design. Refactoring is often preferable to overriding.
- Autodetection runs on the package where an error type is defined (see How Intended Usage Is Detected).
- Overrides force a style based on usage in your code, overriding autodetection.
When possible, improve detection in the defining package by making usage explicit (see Designing Linter-Friendly Packages).
errortype uses short codes to categorize issues:
| Code | Name | Description |
|---|---|---|
et:ret |
Return Mismatch | Error type returned incorrectly (value as pointer or vice versa) |
et:ast |
Assertion Mismatch | Incorrect type in assertion or switch |
et:err |
Argument Mismatch | Incorrect target passed to errors.As-like function |
et:emb |
Ambiguous Usage | Could not determine if error is pointer or value type — use an override |
et:var |
Variable Mismatch | Incorrect assignment in variable declaration starting with Err/err |
et:rcv |
Receiver Mismatch | Unwrap-related method on value error should use value receiver |
| Code | Name | Description |
|---|---|---|
et:cmp |
Pointless Error Comparison | Comparison against &T{} in errors.Is — always false. Use errors.As instead. |
et:equ |
Pointless Comparison | Pointer compared against &T{} — always false. Dereference and compare values. |
| Code | Name | Description |
|---|---|---|
et:unw |
Calling Unwrap | Unwrapping function called inside Is(error) bool — use shallow comparison instead. |
et:sty |
Style Mismatch | Target to errors.As is not an address operation — declare a variable for clarity. |
et:arg |
Invalid Argument | Invalid target to errors.As (also flagged by errorsas). |
et:sig |
Wrong Signature | Unwrap-related method has wrong signature (also flagged by stdmethods). |
et:unu |
Unused Result | Result of errors.Is-like function is unused. |
et:uca |
Unchecked Type Assert | Unchecked type assert might panic on wrapped error — prefer errors.As. |
Add .custom-gcl.yaml to your project:
---
version: v2.8.0
name: golangci-lint
destination: .
plugins:
- module: fillmore-labs.com/errortype
import: fillmore-labs.com/errortype/gclplugin
version: v0.0.9Run golangci-lint custom to build a custom executable. Configure 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
check-unused: falseThen run:
./golangci-lint run .See the module plugin documentation.
- Background on the problem this linter solves
- Why you shouldn't call
UnwrapinIs(error) boolmethods
Licensed under the Apache License 2.0. See LICENSE for details.