errortype is a static analysis tool that performs three 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. -
Error Naming Conventions (opt-in): Checks that sentinel error variables use the
Errprefix (e.g.,ErrNotFound) and structured error types use theErrorsuffix (e.g.,ParseError), following the Go naming conventions.
Getting Started
Installation
Go
go install fillmore-labs.com/errortype@latest
Homebrew
brew install fillmore-labs/tap/errortype
Eget
Install eget, then:
eget fillmore-labs/errortype
Usage
Analyze your entire project:
errortype -naming -style-check ./...
Command-Line Flags
| Flag | Description | Default |
|---|---|---|
-naming |
Check error naming conventions (recommended) | false |
-style-check |
Check for confusing uses of errors.As (recommended) |
false |
-check-unused |
Report unused results of errors.As-like functions |
true |
-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 |
-unchecked-assert |
Diagnose unchecked type asserts on errors | false |
-prefix-filter |
Restrict variable analysis to explicitly named variables (err or Err) |
true |
-c <N> |
Lines of context around each issue (-1 = none, 0 = offending line only) |
-1 |
-test |
Analyze test files | true |
-overrides <file> |
Read type overrides from a YAML file (see Override File) | — |
-suggest <file> |
Append suggestions to an override file (- for stdout) |
— |
-heuristics <list> |
Heuristics to use (“off” to disable) (Experimental) |
var,usage,receivers |
-tracetypes <regex> |
Trace type detection in matching packages (Experimental) | — |
Inconsistent Error Type Usage
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)
How Intended Usage Is Detected
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.
Designing Linter-Friendly Packages
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) )
Overriding Detected Types
When the linter reports ambiguous usage from an imported package you cannot modify, use an override file (see Override File).
Wrapper Functions
errortype automatically detects wrapper functions around errors.Is,
errors.As, and errors.AsType within the analyzed
package. This allows the linter to validate arguments passed to your custom error helpers just as it does for the
standard library functions.
For example, a test helper like this is recognized as an errors.As wrapper:
func RequireErrorsAs(t *testing.T, err error, target any, format string, args ...any) { t.Helper() if !errors.As(err, target) { t.Fatalf(format, args...) } }
The linter detects that err and target are forwarded to errors.As and applies the same checks to call sites of
RequireErrorsAs.
Pointless Comparisons
errortype 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.
Examples
Error Handling with errors.Is
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) } // ... }
Direct Pointer Comparisons
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 { // ... } }
Special Cases for errors.Is
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.
Error Naming Conventions
When enabled with -naming, errortype checks that error names follow
Go conventions:
- Sentinel errors (variables) should start with
Err(orerrfor unexported), e.g.,ErrNotFound. - Structured error types should end with
Error, e.g.,ParseError.
This flag is off by default since it can be noisy on existing codebases, but its usage is recommended for new projects and codebases that already follow these conventions.
See also Go: Naming Errors and the Parlance of Error Types for a detailed discussion.
Override File
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.InconsistentUsage
Review 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.
Overrides vs. Autodetection
- 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).
Diagnostic Code Reference
errortype uses short codes to categorize issues:
Error Type Consistency
| 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:asn |
Assignment Mismatch | Incorrect type in assignment |
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 |
Pointer Comparisons
| 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. |
Naming Conventions
| Code | Name | Description |
|---|---|---|
et:nam |
Variable Naming | Sentinel error name should start with Err (exported) or err. |
et:nam+ |
Type Naming | Structured error type name should end with Error. |
Other Issues
| 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 an errors.As-like function, possible panic. |
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. |
Integration
golangci-lint Module Plugin
Add .custom-gcl.yaml to your project:
--- version: v2.11.3 name: golangci-lint destination: . plugins: - module: fillmore-labs.com/errortype import: fillmore-labs.com/errortype/gclplugin version: v0.0.11
Run 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: naming: true style-check: true deep-is-check: false check-is: true unchecked-assert: false check-unused: false prefix-filter: true overrides: pointer: - your.pkg/a.PointerOverrideError value: - your.pkg/a.ValueOverrideError suppress: - your.pkg/a.SuppressOverrideError
Then run:
./golangci-lint run .See the module plugin documentation.
Links
- Background on the problem this linter solves
- Why you shouldn't call
UnwrapinIs(error) boolmethods - Go Wiki: Error naming
License
Licensed under the Apache License 2.0. See LICENSE for details.