Skip to content

errortype is a Go linter that detects inconsistent usage of custom error types as pointers vs. values.

License

Notifications You must be signed in to change notification settings

fillmore-labs/errortype

Repository files navigation

Errortype

Go Reference Test CodeQL Coverage Go Report Card Codeberg CI License

errortype is a static analysis tool that performs two checks:

  1. Inconsistent Error Type Usage: Ensures error types are used consistently as either pointers or values in returns, type assertions, and errors.As calls.

  2. Pointless Comparisons: Detects comparisons against newly allocated addresses (like errors.Is(err, &url.Error{}) or ptr == &MyStruct{}), which are almost always incorrect.

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 ./...

Command-Line Flags

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)

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:

  1. Overrides: User-defined overrides (see Override File) take highest priority.

  2. Unwrap related methods: Methods like Is, As, and Unwrap with pointer receivers are only visible when the error is used as a pointer.

    func (e *PointerError) Unwrap() error { /* ... */ } // Only visible from error(&PointerError{}).
  3. 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.
  4. Usage in functions: Consistent usage in return statements 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.

  5. 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).

Pointless Comparisons

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.

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.

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: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.

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 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.

Integration

golangci-lint Module Plugin

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.9

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:
          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: false

Then run:

./golangci-lint run .

See the module plugin documentation.

Links

License

Licensed under the Apache License 2.0. See LICENSE for details.

About

errortype is a Go linter that detects inconsistent usage of custom error types as pointers vs. values.

Topics

Resources

License

Stars

Watchers

Forks

Languages