errs

package
v0.1.0-dev7 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 3, 2026 License: Apache-2.0 Imports: 1 Imported by: 0

README

Error Processing (core/errs)

The errs package provides a framework for classifying errors by origin and retryability. It wraps Go's standard errors package — all framework types support errors.Is/errors.As and participate in the standard error chain.

Design

Errors are classified along two axes:

Non-retryable (default) Retryable
User NewUserError (not supported)
Infra (any unclassified error) NewRetryableError
Infra dep NewDependencyError NewRetryableDependencyError

Non-retryable by default. A plain fmt.Errorf(...) is treated as a non-retryable infra error. Retryability must be explicitly opted into by wrapping with NewRetryableError. This prevents accidental infinite retry loops from unclassified errors.

Only infra errors can be retryable. User errors are never retryable — if a user action caused the failure, retrying the same operation will produce the same result. If an error is retryable, it is by definition an infrastructure issue.

Infra by default. Any error that is not explicitly wrapped with NewUserError is an infra error. There is no NewInfraError constructor — infra is the default classification.

Two Routes to a Classification

A returned error reaches IsUserError / IsRetryable / IsDependencyError carrying one of the framework types (*userError / *infraError). It gets there one of two ways:

  1. Explicit wrap by the controller — the controller knows the meaning of the failure and wraps the cause with NewUserError, NewRetryableError, NewDependencyError, or NewRetryableDependencyError before returning.
  2. Automatic wrap by Classify — the controller returns a raw driver/library/sentinel error, and a per-backend Classifier recognises it later in the pipeline (typically inside the consumer) and adds the appropriate framework wrap.

Both routes feed the same downstream helpers; the chain that reaches IsRetryable looks identical regardless of who wrapped it.

Classify and the Classifier Interface

Classifier inspects a single error node and returns a Verdict:

type Classifier interface {
    Classify(err error) Verdict
}

Verdicts: Unknown (this node carries no signal), User, Infra, InfraRetryable, InfraDependency, InfraDependencyRetryable.

Classify(err, classifiers...) is the single, explicit pass that turns a raw chain into a wrapped one. It is called exactly once per chain — typically by the consumer immediately after the controller returns. After that point, callers use only the IsXxx helpers, which are pure type checks.

Classify walks the chain twice:

  1. Pass 1 — framework-wrap check. A cheap type switch looks for an existing *userError / *infraError anywhere in the chain. If found, the chain is already interpretable and Classify returns err unchanged. No classifier is invoked.
  2. Pass 2 — classifier walk. From outermost to innermost node, each registered classifier is asked for a verdict. The first non-Unknown verdict wins and err is wrapped with the matching framework constructor.

If no classifier recognises anything, err is returned unchanged — and behaves as non-retryable infra at the helper layer.

Adding a Backend-Specific Classifier

Backend classifiers live alongside the extension they classify, under core/errs/<backend>/. The canonical examples are core/errs/mysql (MySQL driver errors) and core/errs/generic (transport-agnostic concerns such as context.Canceled).

A classifier:

  • Inspects exactly one node — the err argument passed in. Do not call errors.Is / errors.As from inside Classify; the framework owns the chain walk. Calling it yourself can shadow a deeper-but-different verdict and breaks the controller-override rules described below.
  • Returns Unknown for anything it does not recognise, so the surrounding walker can continue.
  • Is stateless. The convention is to expose a package-level singleton value rather than a constructor:
// core/errs/foo/foo.go
package foo

import "github.com/uber/submitqueue/core/errs"

var Classifier errs.Classifier = classifier{}

type classifier struct{}

func (classifier) Classify(err error) errs.Verdict {
    // Type-assert / sentinel-compare on err directly, never errors.As / errors.Is.
    if fe, ok := err.(*FooError); ok {
        return classifyFooCode(fe.Code)
    }
    return errs.Unknown
}

Servers wire each classifier into the consumer as a vararg. Order matters only when two classifiers might both match a node — earlier classifiers win:

import (
    genericerrs "github.com/uber/submitqueue/core/errs/generic"
    mysqlerrs   "github.com/uber/submitqueue/core/errs/mysql"
)

c := consumer.New(logger, scope, registry,
    genericerrs.Classifier,
    mysqlerrs.Classifier,
)

Tests follow the same shape: assert per-node behaviour against Classifier.Classify(node) directly, and assert end-to-end behaviour by running errs.Classify(err, Classifier) and checking the helpers (IsRetryable, IsUserError, …) on the result. See core/errs/mysql/mysql_test.go and core/errs/generic/generic_test.go.

Overriding Classification from a Controller

Because pass 1 short-circuits on the first framework wrap it finds, an explicit wrap by the controller always wins over any classifier. Use this when the controller has context the classifier cannot — typically when the same low-level error means different things in different call sites.

result, err := c.storage.Get(ctx, id)
if errors.Is(err, storage.ErrNotFound) {
    // This caller treats "not found" as a user error: the user asked for an
    // unknown resource. The mysql classifier never gets a vote because the
    // framework wrap short-circuits pass 1.
    return errs.NewUserError(fmt.Errorf("request %s: %w", id, err))
}
if err != nil {
    // Hand the raw error to Classify — the mysql classifier will recognise
    // deadlocks, lock-wait timeouts, etc. and wrap them as retryable infra.
    return fmt.Errorf("get %s: %w", id, err)
}

Two practical rules fall out of the short-circuit semantics:

  • Wrap with a framework constructor as soon as the controller knows the right verdict. Any wrap added later in the chain still wins, but wrapping early keeps the intent close to the decision.
  • A wrap anywhere in the chain blocks all classifiers — including for nodes deeper than the wrap. If you want a classifier to still get a look at the cause, do not wrap above it. (In practice this is rare: controllers wrap because they have the final answer.)

Extensions Return Plain Go Errors

Extension interfaces (MergeChecker, Storage, Publisher) return standard error values. They may define their own domain-specific sentinel errors (e.g. storage.ErrNotFound, storage.ErrVersionMismatch) but they do not classify errors as user or infra — that is the controller's (and Classify's) job.

This separation keeps extensions reusable across contexts. The same storage.ErrNotFound might be a user error in one controller (user requested a non-existent resource) and an infra error in another (expected record is missing).

Error Chain Compatibility

Framework types preserve the full error chain. Extensions can wrap their own custom errors, and both framework-level and cause-level matching work through errors.Is/errors.As:

// Extension defines a domain error
var ErrNotFound = errors.New("record not found")

// Extension implementation wraps it
return fmt.Errorf("request id=%s: %w", id, ErrNotFound)

// Controller classifies and wraps again
return errs.NewUserError(fmt.Errorf("lookup failed: %w", extensionErr))

// All of these work on the resulting error:
errs.IsUserError(err)             // true — framework classification
errs.IsRetryable(err)             // false — user errors are never retryable
errors.Is(err, ErrNotFound)       // true — cause is in the chain

Helpers

Helper Returns true when
IsUserError(err) err is or wraps a userError
IsRetryable(err) err is or wraps an infra error with the retryable flag set
IsDependencyError(err) err is or wraps an infra error marked as dependency

All three are type-only checks. They do not invoke classifiers — pair them with a preceding Classify call when the controller's error may not carry an explicit wrap.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Classify

func Classify(err error, classifiers ...Classifier) error

Classify is the single, explicit classification pass. It is intended to be called exactly once per error chain — typically by the consumer immediately after a controller returns — and produces a chain that subsequent IsUserError / IsRetryable / IsDependencyError calls can interpret with simple type checks (no further classifier walks).

Semantics:

  • nil in, nil out.
  • If err's chain already carries a framework classification (*userError or *infraError anywhere in the chain), returns err unchanged — the chain is already interpretable by IsUserError / IsRetryable / IsDependencyError.
  • Otherwise, walks the chain from outermost to innermost, asking each classifier per node. The FIRST non-Unknown verdict wins; the outermost such node determines the wrap. err is wrapped with the framework constructor matching that verdict (User -> NewUserError, InfraRetryable -> NewRetryableError, etc.) and the wrapped error is returned.
  • Verdict Infra means "non-retryable infra" — which is already the default behavior for an unwrapped chain, so no wrap is added.
  • If no classifier recognises anything, err is returned unchanged.

Implementation: two passes over the chain. Pass 1 is a cheap type check looking for an existing framework wrap and short-circuits if one is found — no classifier is invoked. Pass 2 runs the configured classifiers per node. Walking the chain is cheap relative to a classifier call, so this avoids running classifiers whenever the chain is already classified deeper down.

NOTE: this central classifier model cannot disambiguate errors of the same underlying type produced by different extensions (e.g. a net.OpError from a mysql connection vs the same type from an HTTP caller would both match the mysql classifier here). Resolving that requires per-extension provenance tagging; intentionally deferred.

func IsDependencyError

func IsDependencyError(err error) bool

IsDependencyError reports whether err is or wraps an infra error marked as originating in a downstream dependency, i.e. an error produced by NewDependencyError or NewRetryableDependencyError. Inspects only the framework types in the chain.

func IsRetryable

func IsRetryable(err error) bool

IsRetryable reports whether err is or wraps an infra error marked retryable, i.e. an error produced by NewRetryableError or NewRetryableDependencyError. Inspects only the framework types in the chain.

func IsUserError

func IsUserError(err error) bool

IsUserError reports whether err is or wraps a user error, i.e. an error produced by NewUserError. Inspects only the framework types in the chain.

func NewDependencyError

func NewDependencyError(cause error) error

NewDependencyError creates a non-retryable dependency infra error wrapping the given cause. A dependency error is an error that is caused by a downstream dependency outside the control of the current system, for example an external build system being down.

func NewRetryableDependencyError

func NewRetryableDependencyError(cause error) error

NewRetryableDependencyError creates a retryable dependency infra error wrapping the given cause. A retryable dependency error is an error that is caused by a downstream dependency outside the control of the current system, for example an external build system being down.

func NewRetryableError

func NewRetryableError(cause error) error

NewRetryableError creates a retryable infra error wrapping the given cause.

func NewUserError

func NewUserError(cause error) error

NewUserError creates a user error wrapping the given cause. A user error is an error that is caused by the user's action or input, for example an invalid input or a merge conflict. User errors are never retryable — only infrastructure errors can be retryable.

Types

type Classifier

type Classifier interface {
	Classify(err error) Verdict
}

Classifier inspects a single error node (not the whole chain) and returns a Verdict. Implementations should return Unknown for nodes they do not recognize so the chain walker can continue down the unwrap chain.

Classifiers must not call errors.As / errors.Is themselves, which would walk the chain and could shadow a classification carried by an outer node (such as a controller's explicit NewUserError wrap). The package-level Classify function owns the walk.

Classifiers are typically stateless; the canonical convention is to expose a package-level singleton value (e.g. mysqlerrs.Classifier) rather than a constructor.

type Verdict

type Verdict int

Verdict is the classification of a single error node, returned by a Classifier. Unknown means the node carries no signal and the chain walker should keep looking; every other value names a terminal classification.

const (
	// Unknown means this node carries no classification. The chain walker
	// will move on to the next node in the unwrap chain.
	Unknown Verdict = iota
	// User means the error is caused by the user's input or action (e.g. a
	// merge conflict or invalid request) and must not be retried.
	User
	// Infra means a non-retryable infrastructure failure: something below the
	// caller broke in a way that retrying will not fix (e.g. a schema or
	// programmer bug). This is the implicit verdict for an unclassified chain,
	// so Classify does not add a wrap for it.
	Infra
	// InfraRetryable means a transient infrastructure failure that is
	// expected to succeed on retry (e.g. a deadlock, lock-wait timeout, or
	// dropped connection).
	InfraRetryable
	// InfraDependency means a non-retryable failure originating in a
	// downstream dependency outside the caller's control (e.g. an external
	// service rejecting the request).
	InfraDependency
	// InfraDependencyRetryable means a transient failure originating in a
	// downstream dependency (e.g. an external service is briefly unavailable)
	// that is expected to succeed on retry.
	InfraDependencyRetryable
)

Directories

Path Synopsis
Package generic provides an errs.Classifier for errors that are not tied to any particular backend.
Package generic provides an errs.Classifier for errors that are not tied to any particular backend.
Package mysql provides an errs.Classifier for errors originating from the go-sql-driver/mysql driver and the standard database/sql + net packages commonly seen when talking to a MySQL backend.
Package mysql provides an errs.Classifier for errors originating from the go-sql-driver/mysql driver and the standard database/sql + net packages commonly seen when talking to a MySQL backend.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL