errors

package module
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Sep 24, 2026 License: MIT Imports: 13 Imported by: 41

README

errors

Give each Go error a type code, an HTTP status, and a gRPC code, and keep them as the error is wrapped, returned, and sent between services.

var ErrUserNotFound = errors.NewKind("USER_NOT_FOUND", errors.ErrNotFound, "user not found")

err := ErrUserNotFound.Wrap(sql.ErrNoRows, "find user 42")

errors.TypeCode(err)                // "USER_NOT_FOUND"
errors.HTTPCode(err)                // 404
errors.GRPCCode(err)                // codes.NotFound
errors.Is(err, ErrUserNotFound)     // true
errors.Is(err, errors.ErrNotFound)  // true
errors.Is(err, sql.ErrNoRows)       // true

Without this, services usually map errors to status codes in each handler. Those mappings drift apart and lose detail at every gRPC hop. With this package, you classify an error once, where it happens:

  • One classification, every transport. HTTP handlers call HTTPCode(err), gRPC servers return SendGRPCError(err), and logs record TypeCode(err).
  • Your own sentinel errors. NewKind creates errors like USER_NOT_FOUND that you check with errors.Is. They also match a broader category, such as ErrNotFound, so generic code can handle them.
  • Classifications cross gRPC. A client gets an error that still matches the server's Kind, with the same type code, HTTP status, and gRPC code. A gateway can forward it to HTTP without its own mapping table.
  • Safe messages for clients. PublicMessage and Public give clients a message without internal details such as SQL errors or file paths.
  • Works with the standard library. Errors work with errors.Is, errors.As, and fmt.Errorf("%w"). The package re-exports Is, As, AsType, Unwrap, Join, and New, so it can replace the standard import.

Installation

go get github.com/stackus/errors

Requires Go 1.26.8 or later.

Contents

Quick start

An error is classified where it enters the application. Code above it adds context, and the caller reads the codes and checks what the error is:

var ErrUserNotFound = errors.NewKind(
    "USER_NOT_FOUND",
    errors.ErrNotFound,
    "user not found",
    errors.WithPublicMessage("The user could not be found"),
)

// Repository: classify the driver error.
func (r *Repo) FindUser(ctx context.Context, id string) (*User, error) {
    row := r.db.QueryRowContext(ctx, "SELECT ... WHERE id = $1", id)
    if err := row.Scan(...); err != nil {
        if errors.Is(err, sql.ErrNoRows) {
            return nil, ErrUserNotFound.Wrap(err, "find user "+id)
        }
        return nil, errors.ErrInternal.Wrap(err, "find user "+id)
    }
    ...
}

// Service: add context. The classification is preserved.
func (s *Service) GetProfile(ctx context.Context, id string) (*Profile, error) {
    user, err := s.repo.FindUser(ctx, id)
    if err != nil {
        return nil, errors.Wrap(err, "get profile")
    }
    ...
}
err := svc.GetProfile(ctx, "42")

fmt.Println(err)                                // get profile: find user 42: sql: no rows in result set
fmt.Println(errors.TypeCode(err))               // USER_NOT_FOUND
fmt.Println(errors.HTTPCode(err))               // 404
fmt.Println(errors.GRPCCode(err))               // NotFound
fmt.Println(errors.Is(err, ErrUserNotFound))    // true
fmt.Println(errors.Is(err, errors.ErrNotFound)) // true
fmt.Println(errors.PublicMessage(err))          // The user could not be found

Defining your errors

Built-in categories

The package includes a category for every gRPC code and for common HTTP statuses. Examples are ErrNotFound, ErrInvalidArgument, ErrConflict, and ErrUnauthorized. See the full table. A category is already a sentinel error:

if user == nil {
    return errors.ErrNotFound
}

Categories are enough for most generic failures, such as "not found", "invalid input", or "internal".

Kinds: your own sentinel errors

Create a Kind for an error that callers need to tell apart from others, or that should have its own code in logs, metrics, and API responses. Declare kinds as package-level variables, just like var ErrX = errors.New(...):

var (
    ErrOrderNotFound = errors.NewKind("ORDER_NOT_FOUND", errors.ErrNotFound, "order not found")
    ErrOrderShipped  = errors.NewKind("ORDER_SHIPPED", errors.ErrFailedPrecondition, "order already shipped")
)

A kind has three parts:

  • Type code. An uppercase string such as ORDER_NOT_FOUND. It can contain A-Z, 0-9, _, and .. It can't be the same as a built-in category's code.
  • Category. A built-in category. The kind inherits its HTTP and gRPC codes, and errors made from the kind match the category with errors.Is.
  • Message. The error text of the kind itself.

Options adjust a kind when it's created:

var ErrPaymentRequired = errors.NewKind(
    "PAYMENT_REQUIRED",
    errors.ErrFailedPrecondition,
    "account has no active subscription",
    errors.WithHTTPCode(http.StatusPaymentRequired),        // 402 instead of 400
    errors.WithPublicMessage("A subscription is required"), // shown to clients
)

var ErrQuotaExceeded = errors.NewKind(
    "QUOTA_EXCEEDED",
    errors.ErrForbidden,
    "storage quota exceeded",
    errors.WithGRPCCode(codes.ResourceExhausted),           // instead of PermissionDenied
)

NewKind panics on invalid input, such as a bad type code or an HTTP code outside 400–599. Kinds are declared at package level, so these mistakes surface when the program starts.

Classifying errors and adding context

Categories and kinds have the same methods for creating errors:

Method Result text Keeps a cause Use it to
Msg(msg) / Msgf(...) msg no create a new error with a detailed message
WithCause(err) err.Error() yes classify an error without changing its text
Wrap(err, msg) / Wrapf msg: err yes classify an error and add context
errors.ErrInvalidArgument.Msg("page size must be positive")
ErrOrderShipped.Msgf("order %d shipped at %s", id, shippedAt)
errors.ErrUnavailable.WithCause(err)
ErrEmailTaken.Wrap(pgErr, "register user")

Wrap, Wrapf, and WithCause return nil when the error is nil. errors.WithKind(err, kind) does the same as kind.WithCause(err). Use it to classify a call's result directly:

return errors.WithKind(s.store.Put(ctx, obj), ErrStorageUnavailable)
Add context without changing the classification

After an error is classified, add context as it moves up through your code. The package-level errors.Wrap / errors.Wrapf and fmt.Errorf with %w all keep the classification and the cause:

err := errors.ErrNotFound.Wrap(sql.ErrNoRows, "find user")
err = fmt.Errorf("repository: %w", err)
err = errors.Wrap(err, "login")

fmt.Println(err)                  // login: repository: find user: sql: no rows in result set
fmt.Println(errors.TypeCode(err)) // NOT_FOUND

When errors.Wrap is given a category directly, it uses the message as the whole error text:

err := errors.Wrap(errors.ErrNotFound, "user 42 not found")
fmt.Println(err) // user 42 not found
Reclassifying

The outermost classification wins. To change what an error means as it crosses a boundary, wrap it with another category or kind:

// A missing user during login is an authentication failure, not a 404.
err := errors.ErrUnauthenticated.Wrap(err, "login")

The original error is still in the chain, so errors.Is(err, ErrUserNotFound) is still true.

Checking errors

Use errors.Is with a kind to match exactly, or with a category to match anything in that category:

switch {
case errors.Is(err, ErrOrderShipped):
    // this specific failure
case errors.Is(err, errors.ErrNotFound):
    // any "not found", including ErrOrderNotFound
}

Two kinds are always different errors, even when they're in the same category.

Use errors.As or the generic errors.AsType to get an error value or an interface from the chain:

if coder, ok := errors.AsType[errors.TypeCoder](err); ok {
    log.Printf("type=%s", coder.TypeCode())
}

Reading codes

errors.TypeCode(err), errors.HTTPCode(err), and errors.GRPCCode(err) look through the whole error chain:

Error TypeCode HTTPCode GRPCCode
category or kind, anywhere in the chain its type code its status its code
gRPC status error (status.Error, client call) from its code and detail from its code and detail its code
context.Canceled CANCELED 408 Canceled
context.DeadlineExceeded DEADLINE_EXCEEDED 504 DeadlineExceeded
errors.Join(...) where all children agree the shared code shared shared
errors.Join(...) where children differ UNKNOWN 500 Unknown
unclassified (errors.New, third-party errors) UNKNOWN 500 Unknown
nil OK 200 OK
ErrOK returned or wrapped as an error UNKNOWN 500 Unknown

If an error has several classifications in its chain, the outermost one is used. To choose the classification of a joined error, wrap the join: errors.ErrInvalidArgument.WithCause(errors.Join(errs...)).

Why UNKNOWN and not INTERNAL?

Unclassified errors are reported as UNKNOWN so that they stay separate from errors you have deliberately marked ErrInternal. When UNKNOWN shows up in your logs, dashboards, or client responses, it points to an error that hasn't been classified yet.

A non-nil error never reports success: HTTPCode(err) is always 400–599 and GRPCCode(err) is never OK when err != nil. Only nil gives OK, 200, and codes.OK.

Public messages

Error text often includes SQL errors, hostnames, or file paths that clients shouldn't see. errors.PublicMessage(err) returns a message that is safe to send. It never returns err.Error(). It uses the first of these that applies:

  1. A message set on this error with errors.WrapPublicMessage(err, msg).
  2. The kind's WithPublicMessage text.
  3. "Internal Server Error" for a 5xx status or an unclassified error.
  4. The HTTP status text, such as "Not Found" or "Conflict".
err := errors.ErrInternal.Wrap(dbErr, "load user")
errors.PublicMessage(err) // "Internal Server Error"

err = errors.WrapPublicMessage(errors.ErrInvalidArgument.Msg("sku ABC-123 failed checksum"),
    "The product code is not valid")
errors.PublicMessage(err) // "The product code is not valid"
err.Error()               // "sku ABC-123 failed checksum"

errors.Public(err) returns a PublicError{Code, Message} that can be serialized directly:

{"code":"USER_NOT_FOUND","message":"The user could not be found"}

HTTP responses

A single helper can turn any error into a response:

func writeError(w http.ResponseWriter, err error) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(errors.HTTPCode(err))
    _ = json.NewEncoder(w).Encode(errors.Public(err))
}

func (h *Handler) GetUser(w http.ResponseWriter, r *http.Request) {
    user, err := h.svc.GetUser(r.Context(), r.PathValue("id"))
    if err != nil {
        slog.ErrorContext(r.Context(), "get user", "err", err, "type", errors.TypeCode(err))
        writeError(w, err)
        return
    }
    ...
}
Error Response
ErrUserNotFound.Wrap(sql.ErrNoRows, "find user 42") 404 {"code":"USER_NOT_FOUND","message":"The user could not be found"}
errors.ErrInternal.Wrap(dialErr, "load user") 500 {"code":"INTERNAL","message":"Internal Server Error"}
errors.ErrUnprocessableEntity.Msg("email: missing @") 422 {"code":"UNPROCESSABLE_ENTITY","message":"Unprocessable Entity"}

gRPC

SendGRPCError converts an error into a gRPC status on the server. ReceiveGRPCError converts it back on the client. The status contains:

  • the gRPC code from GRPCCode(err);
  • the message from PublicMessage(err), so internal text stays on the server;
  • a status detail with the type code, HTTP status, and category;
  • any other details from a status error in the chain, such as errdetails.BadRequest.

On the client, the received error has the sender's type code, category, HTTP status, gRPC code, and public message. That holds across any number of hops.

Interceptors

The grpcerrs subpackage applies these functions to every call:

import "github.com/stackus/errors/grpcerrs"

server := grpc.NewServer(
    grpc.ChainUnaryInterceptor(grpcerrs.UnaryServerInterceptor()),
    grpc.ChainStreamInterceptor(grpcerrs.StreamServerInterceptor()),
)

conn, err := grpc.NewClient(target,
    grpc.WithChainUnaryInterceptor(grpcerrs.UnaryClientInterceptor(registry)),
    grpc.WithChainStreamInterceptor(grpcerrs.StreamClientInterceptor(registry)),
)

Handlers return this package's errors, and callers check them as if the server were local:

// server
func (s *OrderServer) GetOrder(ctx context.Context, req *pb.GetOrderRequest) (*pb.Order, error) {
    order, err := s.svc.GetOrder(ctx, req.GetId())
    if err != nil {
        return nil, err // e.g. orders.ErrOrderNotFound.Wrap(sql.ErrNoRows, "get order 7")
    }
    ...
}

// client
_, err := client.GetOrder(ctx, &pb.GetOrderRequest{Id: 7})

errors.Is(err, orders.ErrOrderNotFound)  // true, with the kind registered
errors.Is(err, errors.ErrNotFound)       // true
errors.TypeCode(err)                     // "ORDER_NOT_FOUND"
errors.HTTPCode(err)                     // 404, so a gateway can pass it on
errors.PublicMessage(err)                // "Order not found"
err.Error()                              // "rpc error: code = NotFound desc = Order not found"

The stream client interceptor converts errors from opening the stream and from Header, SendMsg, RecvMsg, and CloseSend. io.EOF is returned unchanged.

Restoring kinds with a Registry

Every received error matches its built-in category and keeps its type code, HTTP status, and public message. To also match your own kinds with errors.Is, the client needs the same Kind values, usually from a package shared with the server, registered in a Registry:

registry, err := errors.NewRegistry(
    orders.ErrOrderNotFound,
    orders.ErrOrderShipped,
)

A received kind is matched by type code, gRPC code, and category. The local kind's definition supplies the HTTP status. You can pass several registries; they are searched in order.

Plain gRPC status errors

Status errors made with google.golang.org/grpc/status are classified by their code, whether they are returned by a handler or received without the client interceptor, and even when wrapped:

err := fmt.Errorf("get order: %w", status.Error(codes.NotFound, "order 7 not found"))
errors.TypeCode(err)      // "NOT_FOUND"
errors.HTTPCode(err)      // 404
errors.PublicMessage(err) // "Not Found"

A status without this package's detail, such as one from status.Error or from a service that doesn't use this package, keeps its details. Its message is not treated as public, because it may contain internal text. Use WrapPublicMessage to choose the message clients see.

Details
  • The gRPC status code decides the result. If the detail disagrees with it, the detail is ignored.
  • Wrapping a received error with a category or kind reclassifies it. The upstream status details are then not forwarded.
  • SendGRPCError(nil) and ReceiveGRPCError(nil) return nil. ReceiveGRPCError returns errors that aren't gRPC statuses unchanged.

Using your own error types

An existing error type can be classified by implementing one or more of these interfaces. It doesn't need to be wrapped:

type TypeCoder interface { error; TypeCode() string }
type HTTPCoder interface { error; HTTPCode() int }
type GRPCCoder interface { error; GRPCCode() codes.Code }
type ValidationError struct{ Field string }

func (e ValidationError) Error() string    { return "invalid field: " + e.Field }
func (e ValidationError) TypeCode() string { return "VALIDATION_FAILED" }
func (e ValidationError) HTTPCode() int    { return http.StatusUnprocessableEntity }

err := fmt.Errorf("create account: %w", ValidationError{Field: "email"})
errors.TypeCode(err) // "VALIDATION_FAILED"
errors.HTTPCode(err) // 422
errors.GRPCCode(err) // codes.Unknown (not implemented)

Codes a type doesn't provide use the UNKNOWN defaults. An HTTP code outside 400–599 is treated as 500, and a gRPC code of OK as Unknown. Every error in this package implements all three interfaces.

Built-in categories

Categories named for gRPC codes:

Category Type code HTTP gRPC
ErrCanceled CANCELED 408 Canceled
ErrUnknown UNKNOWN 500 Unknown
ErrInvalidArgument INVALID_ARGUMENT 400 InvalidArgument
ErrDeadlineExceeded DEADLINE_EXCEEDED 504 DeadlineExceeded
ErrNotFound NOT_FOUND 404 NotFound
ErrAlreadyExists ALREADY_EXISTS 409 AlreadyExists
ErrPermissionDenied PERMISSION_DENIED 403 PermissionDenied
ErrResourceExhausted RESOURCE_EXHAUSTED 429 ResourceExhausted
ErrFailedPrecondition FAILED_PRECONDITION 400 FailedPrecondition
ErrAborted ABORTED 409 Aborted
ErrOutOfRange OUT_OF_RANGE 422 OutOfRange
ErrUnimplemented UNIMPLEMENTED 501 Unimplemented
ErrInternal INTERNAL 500 Internal
ErrUnavailable UNAVAILABLE 503 Unavailable
ErrDataLoss DATA_LOSS 500 DataLoss
ErrUnauthenticated UNAUTHENTICATED 401 Unauthenticated

Categories named for HTTP statuses:

Category Type code HTTP gRPC
ErrBadRequest BAD_REQUEST 400 InvalidArgument
ErrUnauthorized UNAUTHORIZED 401 Unauthenticated
ErrForbidden FORBIDDEN 403 PermissionDenied
ErrMethodNotAllowed METHOD_NOT_ALLOWED 405 Unimplemented
ErrRequestTimeout REQUEST_TIMEOUT 408 DeadlineExceeded
ErrConflict CONFLICT 409 Aborted
ErrGone GONE 410 NotFound
ErrUnsupportedMediaType UNSUPPORTED_MEDIA_TYPE 415 InvalidArgument
ErrImATeapot IM_A_TEAPOT 418 Unknown
ErrUnprocessableEntity UNPROCESSABLE_ENTITY 422 InvalidArgument
ErrTooManyRequests TOO_MANY_REQUESTS 429 ResourceExhausted
ErrUnavailableForLegalReasons UNAVAILABLE_FOR_LEGAL_REASONS 451 PermissionDenied
ErrInternalServerError INTERNAL_SERVER_ERROR 500 Internal
ErrNotImplemented NOT_IMPLEMENTED 501 Unimplemented
ErrBadGateway BAD_GATEWAY 502 Unavailable
ErrServiceUnavailable SERVICE_UNAVAILABLE 503 Unavailable
ErrGatewayTimeout GATEWAY_TIMEOUT 504 DeadlineExceeded

Each category is a separate error, so ErrBadRequest and ErrInvalidArgument don't match each other with errors.Is, even though both use HTTP 400 and gRPC InvalidArgument. A category received over gRPC keeps its identity when the sender used this package.

ErrOK names the codes of a nil error: OK, 200, and codes.OK. It is not an error to return. Returned or wrapped as an error, it is classified as UNKNOWN, so a failure can't be reported as a success. It can't be used as a kind's category or type code.

More runnable examples are in the package documentation.

Contributing

Pull requests are welcome. Please include tests for behavior changes.

License

MIT

Documentation

Overview

Package errors gives Go errors a type code, an HTTP status, and a gRPC code, so one error value can drive logs, HTTP responses, and gRPC statuses. It works alongside the standard library's errors package. Is, As, AsType, Unwrap, and Join are provided so it can replace that import.

Categories

The Err* constants, such as ErrNotFound and ErrInvalidArgument, are built-in categories of type Error. Each has a type code and HTTP and gRPC mappings (see the constants' comments). A category can be returned directly, matched with Is, or used to classify another error:

return errors.ErrNotFound.Wrap(err, "find user")

Defining application errors

NewKind creates a sentinel error with its own type code inside a category. Declare kinds as package-level variables:

var ErrUserNotFound = errors.NewKind("USER_NOT_FOUND", errors.ErrNotFound, "user not found")

An error created from a kind matches both the kind and its category with Is. A kind inherits its category's HTTP and gRPC codes; WithHTTPCode and WithGRPCCode override them, and WithPublicMessage sets the message shown to clients.

Classifying and adding context

Error and *Kind have the same set of constructors:

  • Msg and Msgf create a new error with the given message and no cause.
  • WithCause classifies an existing error and keeps its message.
  • Wrap and Wrapf classify an existing error and prefix its message.

Classify errors where they enter your application, for example when a database driver returns sql.ErrNoRows. After that, add context with Wrap, Wrapf, or fmt.Errorf with %w. Each keeps the classification and the original cause.

Reading codes

TypeCode, HTTPCode, and GRPCCode return an error's codes. They follow these rules:

  • The outermost classification in the chain wins, so wrapping a classified error with a different category reclassifies it.
  • context.Canceled and context.DeadlineExceeded are classified as ErrCanceled and ErrDeadlineExceeded.
  • A joined error takes the classification its children share. When they differ, or one is unclassified, the join is UNKNOWN.
  • An unclassified error is UNKNOWN, HTTP 500, gRPC Unknown. Unknown errors are kept separate from Internal ones on purpose: they show which failures have not been classified yet.
  • A nil error is OK, HTTP 200, gRPC OK, the codes named by ErrOK.
  • A non-nil error never reports success. ErrOK returned or wrapped as an error is UNKNOWN, HTTP 500, gRPC Unknown.

Custom error types

An error type of your own is classified when it implements TypeCoder, HTTPCoder, or GRPCCoder. Codes it does not provide use the UNKNOWN defaults.

Public messages

An error's text often contains details that clients should not see. PublicMessage returns a client-safe message, and Public returns a code and message pair that can be serialized. To set the message for one error, use WrapPublicMessage.

HTTP

Set the response status from HTTPCode and write Public as the body. See the httpHandler example.

gRPC

On the server, SendGRPCError converts an error into a gRPC status. The status carries the public message, the classification, and any details from a status error in the chain. On the client, ReceiveGRPCError rebuilds a classified error with the sender's type code, category, HTTP code, and public message. To also restore an application Kind's identity, pass a Registry that contains it.

The github.com/stackus/errors/grpcerrs package provides server and client interceptors that apply these functions to every call.

gRPC status errors, such as those made with status.Error, are classified by their code wherever they appear in a chain.

Example

This example follows an error from a repository up through a service. The repository classifies the driver error at the boundary, the service adds context, and the caller reads the codes and checks the error's identity.

package main

import (
	"fmt"

	"github.com/stackus/errors"
)

// ErrUserNotFound is an application sentinel error. It is a distinct error
// identity that also belongs to the built-in ErrNotFound category, so it maps
// to HTTP 404 and gRPC NotFound.
var ErrUserNotFound = errors.NewKind(
	"USER_NOT_FOUND",
	errors.ErrNotFound,
	"user not found",
	errors.WithPublicMessage("The user could not be found"),
)

// errNoRows stands in for a driver error such as sql.ErrNoRows.
var errNoRows = errors.New("sql: no rows in result set")

func main() {
	findUser := func(id string) error {
		// Classify the driver error where it enters the application.
		return ErrUserNotFound.Wrap(errNoRows, "find user "+id)
	}

	getProfile := func(id string) error {
		// Add context on the way up; the classification is preserved.
		return errors.Wrap(findUser(id), "get profile")
	}

	err := getProfile("42")

	fmt.Println(err)
	fmt.Println(errors.TypeCode(err))
	fmt.Println(errors.HTTPCode(err))
	fmt.Println(errors.GRPCCode(err))
	fmt.Println(errors.Is(err, ErrUserNotFound))
	fmt.Println(errors.Is(err, errors.ErrNotFound))
	fmt.Println(errors.Is(err, errNoRows))
	fmt.Println(errors.PublicMessage(err))
}
Output:
get profile: find user 42: sql: no rows in result set
USER_NOT_FOUND
404
NotFound
true
true
true
The user could not be found
Example (HttpHandler)

This example writes an error as an HTTP response. The status comes from HTTPCode, and the body is the client-safe code and message from Public.

package main

import (
	"encoding/json"
	"fmt"
	"net/http"
	"net/http/httptest"

	"github.com/stackus/errors"
)

// ErrUserNotFound is an application sentinel error. It is a distinct error
// identity that also belongs to the built-in ErrNotFound category, so it maps
// to HTTP 404 and gRPC NotFound.
var ErrUserNotFound = errors.NewKind(
	"USER_NOT_FOUND",
	errors.ErrNotFound,
	"user not found",
	errors.WithPublicMessage("The user could not be found"),
)

// errNoRows stands in for a driver error such as sql.ErrNoRows.
var errNoRows = errors.New("sql: no rows in result set")

func main() {
	writeError := func(w http.ResponseWriter, err error) {
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(errors.HTTPCode(err))
		_ = json.NewEncoder(w).Encode(errors.Public(err))
	}

	handle := func(err error) {
		rec := httptest.NewRecorder()
		writeError(rec, err)
		fmt.Print(rec.Code, " ", rec.Body.String())
	}

	handle(ErrUserNotFound.Wrap(errNoRows, "find user 42"))
	handle(errors.ErrInternal.Wrap(errors.New("dial tcp 10.0.0.5:5432: connection refused"), "load user"))
	handle(errors.ErrUnprocessableEntity.Msg("email: missing @"))
}
Output:
404 {"code":"USER_NOT_FOUND","message":"The user could not be found"}
500 {"code":"INTERNAL","message":"Internal Server Error"}
422 {"code":"UNPROCESSABLE_ENTITY","message":"Unprocessable Entity"}

Index

Examples

Constants

This section is empty.

Variables

View Source
var ErrUnsupported = stderrors.ErrUnsupported

ErrUnsupported is the standard library's errors.ErrUnsupported sentinel.

View Source
var File_errorspb_proto protoreflect.FileDescriptor

Functions

func As

func As(err error, target interface{}) bool

As calls the standard library's errors.As.

func AsType added in v0.2.0

func AsType[E error](err error) (E, bool)

AsType returns the first error in err's chain assignable to E, following the standard library's errors.AsType behavior.

Example

AsType finds the first error in the chain that implements an interface, such as TypeCoder.

package main

import (
	"fmt"

	"github.com/stackus/errors"
)

func main() {
	err := fmt.Errorf("checkout: %w", errors.ErrConflict.Msg("cart changed"))

	if coder, ok := errors.AsType[errors.TypeCoder](err); ok {
		fmt.Println(coder.TypeCode())
	}
}
Output:
CONFLICT

func GRPCCode added in v0.0.2

func GRPCCode(err error) codes.Code

GRPCCode returns err's gRPC code, using the same rules as TypeCode. It returns codes.OK for nil, codes.Unknown for an unclassified error, and never codes.OK for a non-nil error.

Example
package main

import (
	"fmt"

	"github.com/stackus/errors"
)

func main() {
	fmt.Println(errors.GRPCCode(errors.ErrNotFound))
	fmt.Println(errors.GRPCCode(errors.ErrUnauthorized.Msg("token expired")))
	fmt.Println(errors.GRPCCode(errors.New("something failed")))
	fmt.Println(errors.GRPCCode(nil))
}
Output:
NotFound
Unauthenticated
Unknown
OK

func HTTPCode added in v0.0.2

func HTTPCode(err error) int

HTTPCode returns err's HTTP status, using the same rules as TypeCode. It returns 200 for nil, 500 for an unclassified error, and a status in 400–599 for any non-nil error.

Example
package main

import (
	"context"
	"fmt"

	"github.com/stackus/errors"
)

func main() {
	fmt.Println(errors.HTTPCode(errors.ErrNotFound))
	fmt.Println(errors.HTTPCode(errors.ErrBadRequest.Msg("missing name")))
	fmt.Println(errors.HTTPCode(errors.New("something failed")))
	fmt.Println(errors.HTTPCode(fmt.Errorf("query: %w", context.Canceled)))
	fmt.Println(errors.HTTPCode(nil))
}
Output:
404
400
500
408
200

func Is

func Is(err, target error) bool

Is calls the standard library's errors.Is.

func Join added in v0.1.7

func Join(errs ...error) error

Join calls the standard library's errors.Join.

Example

When a joined error contains different classifications, the join is UNKNOWN. Classify the join itself to choose the result.

package main

import (
	"fmt"

	"github.com/stackus/errors"
)

func main() {
	joined := errors.Join(
		errors.ErrNotFound.Msg("user not found"),
		errors.ErrForbidden.Msg("project is private"),
	)
	fmt.Println(errors.TypeCode(joined))

	err := errors.ErrInvalidArgument.WithCause(joined)
	fmt.Println(errors.TypeCode(err))
	fmt.Println(errors.Is(err, errors.ErrForbidden))
}
Output:
UNKNOWN
INVALID_ARGUMENT
true

func New added in v0.2.0

func New(message string) error

New returns an ordinary, unclassified error with the given message.

func PublicMessage added in v0.2.0

func PublicMessage(err error) string

PublicMessage returns a message for err that is safe to show clients. It never returns err.Error(). It uses the first of these that applies:

  1. A message set with WrapPublicMessage.
  2. The message set with WithPublicMessage on err's Kind.
  3. "Internal Server Error" for a 5xx HTTP code or an unclassified error.
  4. The HTTP status text for err's code, such as "Not Found".

It returns an empty string for nil.

Example

PublicMessage never returns an error's internal text. Server errors become "Internal Server Error", client errors use their HTTP status text, and a kind can supply its own message.

package main

import (
	"fmt"

	"github.com/stackus/errors"
)

func main() {
	ErrAccountLocked := errors.NewKind(
		"ACCOUNT_LOCKED",
		errors.ErrPermissionDenied,
		"account locked after failed logins",
		errors.WithPublicMessage("Your account is locked"),
	)

	fmt.Println(errors.PublicMessage(errors.ErrInternal.Wrap(errors.New("password authentication failed for user app"), "connect")))
	fmt.Println(errors.PublicMessage(errors.ErrNotFound.Msg("row 42 missing from users")))
	fmt.Println(errors.PublicMessage(ErrAccountLocked.Msg("5 failed logins from 10.0.0.9")))
	fmt.Println(errors.PublicMessage(errors.New("unclassified")))
}
Output:
Internal Server Error
Not Found
Your account is locked
Internal Server Error

func ReceiveGRPCError

func ReceiveGRPCError(err error, registries ...*Registry) error

ReceiveGRPCError rebuilds a classified error from a gRPC status error, such as one returned by a client call. Call it on every error a client call returns, usually from a client interceptor such as the ones in the grpcerrs subpackage.

The result is classified from the status code and the ErrorType detail that SendGRPCError attaches:

  • A built-in category, such as ErrBadRequest, is restored as sent, with the same codes and a match with Is.
  • A Kind found in one of the registries is restored as sent. The result matches the Kind and its category, and has the local Kind's codes. Registries are searched in order, and the first match is used.
  • A Kind that isn't registered keeps its type code, category, and HTTP code. The result matches the category, but not any Kind.

PublicMessage of the result is the message the sender sent. The status code decides the result. When a detail disagrees with it, the detail is ignored. A status without a detail, for example from a server that does not use this package, is classified by its status code alone, and its message is not treated as public.

The result's Error text is the status error's text, and its GRPCStatus method returns the received status, details included.

It returns nil for nil and returns an error that isn't a gRPC status unchanged.

Example

With a registry, the received error matches the same Kind the server sent. Without one, it keeps the type code, category, HTTP code, and public message, but it doesn't match the Kind.

package main

import (
	"fmt"

	"github.com/stackus/errors"
)

func main() {
	ErrCartExpired := errors.NewKind("CART_EXPIRED", errors.ErrFailedPrecondition, "cart expired")
	registry, _ := errors.NewRegistry(ErrCartExpired)

	sent := errors.SendGRPCError(ErrCartExpired.Msg("cart 9 expired at 12:00"))

	withRegistry := errors.ReceiveGRPCError(sent, registry)
	fmt.Println(errors.Is(withRegistry, ErrCartExpired), errors.TypeCode(withRegistry), errors.HTTPCode(withRegistry))

	withoutRegistry := errors.ReceiveGRPCError(sent)
	fmt.Println(errors.Is(withoutRegistry, ErrCartExpired), errors.Is(withoutRegistry, errors.ErrFailedPrecondition), errors.TypeCode(withoutRegistry))

	fmt.Println(errors.PublicMessage(withRegistry))
}
Output:
true CART_EXPIRED 400
false true CART_EXPIRED
Bad Request

func SendGRPCError

func SendGRPCError(err error) error

SendGRPCError converts err into a gRPC status error for a server to return. The status code is GRPCCode(err), and the message is PublicMessage(err), so internal text never leaves the server. The type code, HTTP code, and category are attached as an ErrorType status detail, which ReceiveGRPCError reads on the client.

Call it on every error a handler returns, usually from a server interceptor such as the ones in the grpcerrs subpackage. It returns nil for a nil err.

A gRPC status error in err's chain, whether made with google.golang.org/grpc/status or received from another service, is classified by its code and detail. Unless an outer category or kind reclassifies it, its other status details are sent along too.

Example

SendGRPCError turns an error into a gRPC status error. The status message is the public message, so internal details stay on the server.

package main

import (
	"fmt"

	"github.com/stackus/errors"
	"google.golang.org/grpc/status"
)

func main() {
	err := errors.ErrInternal.Wrap(errors.New("redis: connection pool exhausted"), "load session")

	s, _ := status.FromError(errors.SendGRPCError(err))
	fmt.Println(s.Code())
	fmt.Println(s.Message())
}
Output:
Internal
Internal Server Error
Example (StatusError)

A status error from google.golang.org/grpc/status is classified by its code, so it can be returned from a handler or inspected directly.

package main

import (
	"fmt"

	"github.com/stackus/errors"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

func main() {
	err := fmt.Errorf("get order: %w", status.Error(codes.NotFound, "order 7 not found"))
	fmt.Println(errors.TypeCode(err), errors.HTTPCode(err))

	s, _ := status.FromError(errors.SendGRPCError(err))
	fmt.Println(s.Code(), s.Message())
}
Output:
NOT_FOUND 404
NotFound Not Found

func TypeCode added in v0.0.2

func TypeCode(err error) string

TypeCode returns err's type code. It uses the outermost classification in err's chain; a joined error with different classifications is "UNKNOWN". It returns "UNKNOWN" for an unclassified error and "OK" for nil. A non-nil error never returns "OK", even ErrOK itself. See the package documentation for the full rules.

Example

TypeCode finds the classification anywhere in an error's chain. Errors without a classification are UNKNOWN, and context errors map to their matching categories.

package main

import (
	"context"
	"fmt"

	"github.com/stackus/errors"
)

func main() {
	fmt.Println(errors.TypeCode(errors.ErrNotFound))
	fmt.Println(errors.TypeCode(fmt.Errorf("load: %w", errors.ErrForbidden.Msg("no access"))))
	fmt.Println(errors.TypeCode(errors.New("something failed")))
	fmt.Println(errors.TypeCode(context.DeadlineExceeded))
	fmt.Println(errors.TypeCode(nil))
}
Output:
NOT_FOUND
FORBIDDEN
UNKNOWN
DEADLINE_EXCEEDED
OK

func Unwrap

func Unwrap(err error) error

Unwrap calls the standard library's errors.Unwrap.

func WithKind added in v0.2.0

func WithKind(err error, kind *Kind) error

WithKind classifies err with kind and keeps err's text and its place in the chain. It is the same as kind.WithCause(err), except that it returns nil for a nil err before checking kind, so it can wrap a call's result directly. It panics if kind is nil and err is not.

Example

WithKind classifies an error with a kind and keeps its message. It returns nil for a nil error, so it can wrap a call's result directly.

package main

import (
	"fmt"

	"github.com/stackus/errors"
)

func main() {
	ErrStorage := errors.NewKind("STORAGE_UNAVAILABLE", errors.ErrUnavailable, "storage unavailable")

	save := func(fail bool) error {
		if fail {
			return errors.New("s3: request timed out")
		}
		return nil
	}

	fmt.Println(errors.WithKind(save(false), ErrStorage))

	err := errors.WithKind(save(true), ErrStorage)
	fmt.Println(err)
	fmt.Println(errors.TypeCode(err))
}
Output:
<nil>
s3: request timed out
STORAGE_UNAVAILABLE

func Wrap

func Wrap(err error, msg string) error

Wrap adds msg to err. The result keeps err's classification, and Is and As can still reach err.

If err is a category such as ErrNotFound, msg becomes the complete error text, so errors.Wrap(errors.ErrNotFound, "user 42 not found") reads "user 42 not found". Any other error, including a Kind, reads "msg: err". Wrap returns err unchanged when msg is empty, and nil when err is nil.

To classify or reclassify err, use a category's or a kind's Wrap method instead.

Example

Wrapping a built-in category directly uses the message as the complete error text. The result still matches the category.

package main

import (
	"fmt"

	"github.com/stackus/errors"
)

func main() {
	err := errors.Wrap(errors.ErrNotFound, "user 42 not found")
	fmt.Println(err)
	fmt.Println(errors.Is(err, errors.ErrNotFound))
}
Output:
user 42 not found
true
Example (Multiple)

Each additional Wrap adds another prefix.

package main

import (
	"fmt"

	"github.com/stackus/errors"
)

func main() {
	err := errors.Wrap(errors.ErrNotFound, "original message")
	err = errors.Wrap(err, "prefixed message")
	fmt.Println(err)
}
Output:
prefixed message: original message
Example (OrdinaryError)

Wrapping any other error produces "msg: cause" and keeps the cause and its classification reachable.

package main

import (
	"fmt"

	"github.com/stackus/errors"
)

func main() {
	err := errors.Wrap(errors.ErrForbidden.Msg("project is archived"), "update project")
	fmt.Println(err)
	fmt.Println(errors.TypeCode(err))
}
Output:
update project: project is archived
FORBIDDEN

func WrapPublicMessage added in v0.2.0

func WrapPublicMessage(err error, message string) error

WrapPublicMessage sets the message that PublicMessage returns for err, for cases where one error needs a message its kind or category doesn't provide. err's text, codes, and identity don't change. If err is later wrapped by a category or kind, that outer classification's message is used instead. WrapPublicMessage returns nil for a nil err and panics when message is empty for a non-nil err.

Example

WrapPublicMessage sets a client-facing message for a single error. The error text and identity don't change.

package main

import (
	"fmt"

	"github.com/stackus/errors"
)

func main() {
	err := errors.ErrInvalidArgument.Msg("sku ABC-123 failed checksum")
	err = errors.WrapPublicMessage(err, "The product code is not valid")

	fmt.Println(err)
	fmt.Println(errors.PublicMessage(err))
	fmt.Println(errors.Is(err, errors.ErrInvalidArgument))
}
Output:
sku ABC-123 failed checksum
The product code is not valid
true

func Wrapf

func Wrapf(err error, format string, args ...any) error

Wrapf formats msg and calls Wrap. It returns nil when err is nil.

Types

type Error

type Error string

Error is a built-in error category. Its string value is its type code.

Use the Err* constants in three ways:

  • Return one directly: return errors.ErrNotFound
  • Match against one: errors.Is(err, errors.ErrNotFound)
  • Classify an error: errors.ErrNotFound.Wrap(err, "find user")

Errors built from a category match it with Is, including a Kind created in that category. Values that are not registered categories report HTTP 500 and gRPC Unknown.

const (
	ErrOK                 Error = "OK"                  // HTTP: 200 GRPC: codes.OK
	ErrCanceled           Error = "CANCELED"            // HTTP: 408 GRPC: codes.Canceled
	ErrUnknown            Error = "UNKNOWN"             // HTTP: 500 GRPC: codes.Unknown
	ErrInvalidArgument    Error = "INVALID_ARGUMENT"    // HTTP: 400 GRPC: codes.InvalidArgument
	ErrDeadlineExceeded   Error = "DEADLINE_EXCEEDED"   // HTTP: 504 GRPC: codes.DeadlineExceeded
	ErrNotFound           Error = "NOT_FOUND"           // HTTP: 404 GRPC: codes.NotFound
	ErrAlreadyExists      Error = "ALREADY_EXISTS"      // HTTP: 409 GRPC: codes.AlreadyExists
	ErrPermissionDenied   Error = "PERMISSION_DENIED"   // HTTP: 403 GRPC: codes.PermissionDenied
	ErrResourceExhausted  Error = "RESOURCE_EXHAUSTED"  // HTTP: 429 GRPC: codes.ResourceExhausted
	ErrFailedPrecondition Error = "FAILED_PRECONDITION" // HTTP: 400 GRPC: codes.FailedPrecondition
	ErrAborted            Error = "ABORTED"             // HTTP: 409 GRPC: codes.Aborted
	ErrOutOfRange         Error = "OUT_OF_RANGE"        // HTTP: 422 GRPC: codes.OutOfRange
	ErrUnimplemented      Error = "UNIMPLEMENTED"       // HTTP: 501 GRPC: codes.Unimplemented
	ErrInternal           Error = "INTERNAL"            // HTTP: 500 GRPC: codes.Internal
	ErrUnavailable        Error = "UNAVAILABLE"         // HTTP: 503 GRPC: codes.Unavailable
	ErrDataLoss           Error = "DATA_LOSS"           // HTTP: 500 GRPC: codes.DataLoss
	ErrUnauthenticated    Error = "UNAUTHENTICATED"     // HTTP: 401 GRPC: codes.Unauthenticated
)

Built-in categories named for gRPC codes, one per code. Choose these when gRPC is your main transport or when the gRPC meaning is the more precise one. Each constant is a sentinel for Is and can classify other errors with methods such as Error.Wrap.

ErrOK is not a registered category. It names the codes of a nil error: OK, HTTP 200, and codes.OK. It is not an error to return; a non-nil error never reports success, so ErrOK returned or wrapped as an error is classified as UNKNOWN. It cannot be used as a Kind's category or type code.

const (
	ErrBadRequest                 Error = "BAD_REQUEST"                   // HTTP: 400 GRPC: codes.InvalidArgument
	ErrUnauthorized               Error = "UNAUTHORIZED"                  // HTTP: 401 GRPC: codes.Unauthenticated
	ErrForbidden                  Error = "FORBIDDEN"                     // HTTP: 403 GRPC: codes.PermissionDenied
	ErrMethodNotAllowed           Error = "METHOD_NOT_ALLOWED"            // HTTP: 405 GRPC: codes.Unimplemented
	ErrRequestTimeout             Error = "REQUEST_TIMEOUT"               // HTTP: 408 GRPC: codes.DeadlineExceeded
	ErrConflict                   Error = "CONFLICT"                      // HTTP: 409 GRPC: codes.Aborted
	ErrGone                       Error = "GONE"                          // HTTP: 410 GRPC: codes.NotFound
	ErrUnsupportedMediaType       Error = "UNSUPPORTED_MEDIA_TYPE"        // HTTP: 415 GRPC: codes.InvalidArgument
	ErrImATeapot                  Error = "IM_A_TEAPOT"                   // HTTP: 418 GRPC: codes.Unknown
	ErrUnprocessableEntity        Error = "UNPROCESSABLE_ENTITY"          // HTTP: 422 GRPC: codes.InvalidArgument
	ErrTooManyRequests            Error = "TOO_MANY_REQUESTS"             // HTTP: 429 GRPC: codes.ResourceExhausted
	ErrUnavailableForLegalReasons Error = "UNAVAILABLE_FOR_LEGAL_REASONS" // HTTP: 451 GRPC: codes.PermissionDenied
	ErrInternalServerError        Error = "INTERNAL_SERVER_ERROR"         // HTTP: 500 GRPC: codes.Internal
	ErrNotImplemented             Error = "NOT_IMPLEMENTED"               // HTTP: 501 GRPC: codes.Unimplemented
	ErrBadGateway                 Error = "BAD_GATEWAY"                   // HTTP: 502 GRPC: codes.Unavailable
	ErrServiceUnavailable         Error = "SERVICE_UNAVAILABLE"           // HTTP: 503 GRPC: codes.Unavailable
	ErrGatewayTimeout             Error = "GATEWAY_TIMEOUT"               // HTTP: 504 GRPC: codes.DeadlineExceeded
)

Built-in categories named for common HTTP client and server statuses. Choose these when HTTP is your main transport. Each one maps to the closest gRPC code. They are separate categories from the gRPC-named ones, so ErrBadRequest does not match ErrInvalidArgument.

func (Error) Error

func (e Error) Error() string

Error returns the category's type code as its error text.

func (Error) GRPCCode

func (e Error) GRPCCode() codes.Code

GRPCCode returns the category's gRPC code, codes.OK for ErrOK, or Unknown if it is unregistered.

func (Error) HTTPCode

func (e Error) HTTPCode() int

HTTPCode returns the category's HTTP status, 200 for ErrOK, or 500 if it is unregistered.

func (Error) Msg added in v0.1.5

func (e Error) Msg(msg string) error

Msg returns a classified error with msg as its full error text and no cause.

Example

Msg creates a classified error from scratch.

package main

import (
	"fmt"

	"github.com/stackus/errors"
)

func main() {
	err := errors.ErrInvalidArgument.Msg("page size must be positive")
	fmt.Println(err)
	fmt.Println(errors.GRPCCode(err))
}
Output:
page size must be positive
InvalidArgument

func (Error) Msgf added in v0.1.5

func (e Error) Msgf(format string, args ...any) error

Msgf formats a message and returns the same classified result as Error.Msg.

Example
package main

import (
	"fmt"

	"github.com/stackus/errors"
)

func main() {
	err := errors.ErrTooManyRequests.Msgf("limit of %d requests per minute exceeded", 60)
	fmt.Println(err)
	fmt.Println(errors.HTTPCode(err))
}
Output:
limit of 60 requests per minute exceeded
429

func (Error) TypeCode added in v0.0.2

func (e Error) TypeCode() string

TypeCode returns the category's type code.

func (Error) WithCause added in v0.2.0

func (e Error) WithCause(cause error) error

WithCause classifies cause and uses cause.Error() as the error text. It returns nil when cause is nil.

Example

WithCause classifies an error without changing its text.

package main

import (
	"fmt"

	"github.com/stackus/errors"
)

func main() {
	err := errors.ErrUnavailable.WithCause(errors.New("dial tcp: connection refused"))
	fmt.Println(err)
	fmt.Println(errors.TypeCode(err))
}
Output:
dial tcp: connection refused
UNAVAILABLE

func (Error) Wrap added in v0.1.4

func (e Error) Wrap(cause error, msg string) error

Wrap classifies cause and prefixes its error text with msg. An empty msg leaves the cause's text unchanged. It returns nil when cause is nil.

Example

Use a category's Wrap to classify an error that came from somewhere else, such as a database driver or another library.

package main

import (
	"fmt"

	"github.com/stackus/errors"
)

// errNoRows stands in for a driver error such as sql.ErrNoRows.
var errNoRows = errors.New("sql: no rows in result set")

func main() {
	err := errors.ErrNotFound.Wrap(errNoRows, "find order 7")
	fmt.Println(err)
	fmt.Println(errors.HTTPCode(err))
	fmt.Println(errors.Is(err, errors.ErrNotFound))
	fmt.Println(errors.Is(err, errNoRows))
}
Output:
find order 7: sql: no rows in result set
404
true
true

func (Error) Wrapf added in v0.1.4

func (e Error) Wrapf(cause error, format string, args ...any) error

Wrapf formats msg and calls Error.Wrap. It returns nil when cause is nil.

type ErrorType added in v0.0.2

type ErrorType struct {

	// TypeCode identifies a built-in category or application kind.
	TypeCode string `protobuf:"bytes,1,opt,name=TypeCode,proto3" json:"TypeCode,omitempty"`
	// HTTPCode is the HTTP status associated with TypeCode.
	HTTPCode int64 `protobuf:"varint,2,opt,name=HTTPCode,proto3" json:"HTTPCode,omitempty"`
	// GRPCCode is the gRPC code associated with TypeCode.
	GRPCCode int64 `protobuf:"varint,3,opt,name=GRPCCode,proto3" json:"GRPCCode,omitempty"`
	// Category is the built-in category associated with the classification.
	Category string `protobuf:"bytes,4,opt,name=Category,proto3" json:"Category,omitempty"`
	// contains filtered or unexported fields
}

ErrorType carries classification metadata in a gRPC status detail. Receivers check its gRPC code against the actual status and validate known types against local definitions.

func (*ErrorType) Descriptor deprecated added in v0.0.2

func (*ErrorType) Descriptor() ([]byte, []int)

Deprecated: Use ErrorType.ProtoReflect.Descriptor instead.

func (*ErrorType) GetCategory added in v0.2.0

func (x *ErrorType) GetCategory() string

func (*ErrorType) GetGRPCCode added in v0.0.2

func (x *ErrorType) GetGRPCCode() int64

func (*ErrorType) GetHTTPCode added in v0.0.2

func (x *ErrorType) GetHTTPCode() int64

func (*ErrorType) GetTypeCode added in v0.0.2

func (x *ErrorType) GetTypeCode() string

func (*ErrorType) ProtoMessage added in v0.0.2

func (*ErrorType) ProtoMessage()

func (*ErrorType) ProtoReflect added in v0.0.2

func (x *ErrorType) ProtoReflect() protoreflect.Message

func (*ErrorType) Reset added in v0.0.2

func (x *ErrorType) Reset()

func (*ErrorType) String added in v0.0.2

func (x *ErrorType) String() string

type GRPCCoder

type GRPCCoder interface {
	error
	GRPCCode() codes.Code
}

GRPCCoder is implemented by errors that report a gRPC status code. See TypeCoder.

type HTTPCoder

type HTTPCoder interface {
	error
	HTTPCode() int
}

HTTPCoder is implemented by errors that report an HTTP status code. See TypeCoder.

Example

Your own error types are classified by implementing the coder interfaces. Codes a type does not provide use the defaults; here the gRPC code is Unknown because ValidationError does not implement GRPCCoder.

package main

import (
	"fmt"
	"net/http"

	"github.com/stackus/errors"
)

// ValidationError is an application error type. It implements TypeCoder and
// HTTPCoder, so the package can classify it without wrapping.
type ValidationError struct {
	Field string
}

func (e ValidationError) Error() string    { return "invalid field: " + e.Field }
func (e ValidationError) TypeCode() string { return "VALIDATION_FAILED" }
func (e ValidationError) HTTPCode() int    { return http.StatusUnprocessableEntity }

func main() {
	err := fmt.Errorf("create account: %w", ValidationError{Field: "email"})

	fmt.Println(errors.TypeCode(err))
	fmt.Println(errors.HTTPCode(err))
	fmt.Println(errors.GRPCCode(err))
	fmt.Println(errors.PublicMessage(err))
}
Output:
VALIDATION_FAILED
422
Unknown
Unprocessable Entity

type Kind added in v0.2.0

type Kind struct {
	// contains filtered or unexported fields
}

Kind is an application-specific sentinel error inside a built-in Error category. Create kinds with NewKind and declare them as package-level variables:

var ErrUserNotFound = errors.NewKind("USER_NOT_FOUND", errors.ErrNotFound, "user not found")

Kinds are compared by pointer, so two kinds with the same type code are still different errors. Errors created from a kind (with Msg, Wrap, WithCause, and so on) match both the kind and its category with Is. Its type code, HTTP code, gRPC code, and public message are set when the kind is created and cannot change.

func NewKind added in v0.2.0

func NewKind(typeCode string, category Error, message string, options ...KindOption) *Kind

NewKind creates an application-specific error with a distinct type code. The type code must start with an uppercase letter and contain only uppercase letters, digits, underscores, or periods. Its category supplies default HTTP and gRPC codes; options can override them. message is the error text of the kind itself and of errors made with its WithCause method.

NewKind panics for an invalid type code or one that matches a built-in category or ErrOK, an unregistered category (including ErrOK), an empty message, an HTTP code outside 400–599, or a gRPC code of OK. Kinds are normally declared as package-level variables, so these mistakes surface when the program starts.

Example

Declare kinds as package-level variables and use them like any other sentinel error. Each kind is its own identity, and it also matches the category it belongs to.

package main

import (
	"fmt"

	"github.com/stackus/errors"
)

func main() {
	var (
		ErrOrderNotFound   = errors.NewKind("ORDER_NOT_FOUND", errors.ErrNotFound, "order not found")
		ErrInvoiceNotFound = errors.NewKind("INVOICE_NOT_FOUND", errors.ErrNotFound, "invoice not found")
	)

	err := ErrOrderNotFound.Msgf("order %d not found", 7)

	fmt.Println(errors.Is(err, ErrOrderNotFound))
	fmt.Println(errors.Is(err, ErrInvoiceNotFound))
	fmt.Println(errors.Is(err, errors.ErrNotFound))
	fmt.Println(errors.TypeCode(err), errors.HTTPCode(err), errors.GRPCCode(err))
}
Output:
true
false
true
ORDER_NOT_FOUND 404 NotFound
Example (Options)

Options override the codes a kind inherits from its category and set the message that clients see.

package main

import (
	"fmt"
	"net/http"

	"github.com/stackus/errors"
	"google.golang.org/grpc/codes"
)

func main() {
	ErrPaymentRequired := errors.NewKind(
		"PAYMENT_REQUIRED",
		errors.ErrFailedPrecondition,
		"account has no active subscription",
		errors.WithHTTPCode(http.StatusPaymentRequired),
		errors.WithPublicMessage("A subscription is required"),
	)
	ErrQuotaExceeded := errors.NewKind(
		"QUOTA_EXCEEDED",
		errors.ErrForbidden,
		"storage quota exceeded",
		errors.WithGRPCCode(codes.ResourceExhausted),
	)

	fmt.Println(errors.HTTPCode(ErrPaymentRequired), errors.GRPCCode(ErrPaymentRequired))
	fmt.Println(errors.PublicMessage(ErrPaymentRequired))
	fmt.Println(errors.HTTPCode(ErrQuotaExceeded), errors.GRPCCode(ErrQuotaExceeded))
	fmt.Println(errors.PublicMessage(ErrQuotaExceeded))
}
Output:
402 FailedPrecondition
A subscription is required
403 ResourceExhausted
Forbidden

func (*Kind) Error added in v0.2.0

func (k *Kind) Error() string

Error returns the Kind's default error message.

func (*Kind) GRPCCode added in v0.2.0

func (k *Kind) GRPCCode() codes.Code

GRPCCode returns the Kind's gRPC status code.

func (*Kind) HTTPCode added in v0.2.0

func (k *Kind) HTTPCode() int

HTTPCode returns the Kind's HTTP status.

func (*Kind) Is added in v0.2.0

func (k *Kind) Is(target error) bool

Is reports whether target is this Kind or this Kind's category.

func (*Kind) Msg added in v0.2.0

func (k *Kind) Msg(msg string) error

Msg returns this Kind with msg as its full error text and no cause.

func (*Kind) Msgf added in v0.2.0

func (k *Kind) Msgf(format string, args ...any) error

Msgf formats a message and calls Kind.Msg.

Example

Msgf creates an error of the kind with a detailed message and no cause.

package main

import (
	"fmt"

	"github.com/stackus/errors"
)

func main() {
	ErrInsufficientFunds := errors.NewKind("INSUFFICIENT_FUNDS", errors.ErrFailedPrecondition, "insufficient funds")

	err := ErrInsufficientFunds.Msgf("balance %d is less than %d", 30, 45)

	fmt.Println(err)
	fmt.Println(errors.TypeCode(err))
}
Output:
balance 30 is less than 45
INSUFFICIENT_FUNDS

func (*Kind) PublicError added in v0.2.0

func (k *Kind) PublicError() string

PublicError returns the message set with WithPublicMessage, or an empty string if none was set. Use PublicMessage to get a client message for any error.

func (*Kind) TypeCode added in v0.2.0

func (k *Kind) TypeCode() string

TypeCode returns the Kind's application type code.

func (*Kind) WithCause added in v0.2.0

func (k *Kind) WithCause(cause error) error

WithCause wraps cause with this Kind and uses cause.Error() as the error text. It returns nil when cause is nil.

func (*Kind) Wrap added in v0.2.0

func (k *Kind) Wrap(cause error, msg string) error

Wrap wraps cause with this Kind and prefixes its error text with msg. It returns nil when cause is nil.

Example

Wrap classifies a cause with the kind and adds context to its message.

package main

import (
	"fmt"

	"github.com/stackus/errors"
)

func main() {
	ErrEmailTaken := errors.NewKind("EMAIL_TAKEN", errors.ErrAlreadyExists, "email already registered")

	cause := errors.New(`pq: duplicate key value violates unique constraint "users_email_key"`)
	err := ErrEmailTaken.Wrap(cause, "register user")

	fmt.Println(err)
	fmt.Println(errors.Is(err, ErrEmailTaken))
	fmt.Println(errors.HTTPCode(err))
}
Output:
register user: pq: duplicate key value violates unique constraint "users_email_key"
true
409

func (*Kind) Wrapf added in v0.2.0

func (k *Kind) Wrapf(cause error, format string, args ...any) error

Wrapf formats msg and calls Kind.Wrap. It returns nil when cause is nil.

type KindOption added in v0.2.0

type KindOption func(*Kind)

KindOption configures a Kind during NewKind.

func WithGRPCCode added in v0.2.0

func WithGRPCCode(grpcCode codes.Code) KindOption

WithGRPCCode overrides the category's gRPC code for a new Kind. NewKind panics if the resulting code is OK.

func WithHTTPCode added in v0.2.0

func WithHTTPCode(httpCode int) KindOption

WithHTTPCode overrides the category's HTTP status for a new Kind. NewKind panics if the resulting status is outside 400–599.

func WithPublicMessage added in v0.2.0

func WithPublicMessage(publicMessage string) KindOption

WithPublicMessage sets the message that PublicMessage and SendGRPCError show clients for this Kind. Without it, clients see the HTTP status text for 4xx codes and "Internal Server Error" for 5xx codes. It panics if publicMessage is empty.

type PublicError added in v0.2.0

type PublicError struct {
	Code    string `json:"code" xml:"code" yaml:"code" msgpack:"code"`
	Message string `json:"message" xml:"message" yaml:"message" msgpack:"message"`
}

PublicError is the type code and client message for an error, as returned by Public. It serializes without the error's internal text, for example as JSON:

{"code":"USER_NOT_FOUND","message":"The user could not be found"}

func Public added in v0.2.0

func Public(err error) PublicError

Public returns err's type code and client-facing message. For nil, the code is "OK" and the message is empty.

Example

Public returns a value that can be serialized directly in a response.

package main

import (
	"encoding/json"
	"fmt"

	"github.com/stackus/errors"
)

func main() {
	err := errors.ErrConflict.Msg("version 3 does not match 4")

	b, _ := json.Marshal(errors.Public(err))
	fmt.Println(string(b))
}
Output:
{"code":"CONFLICT","message":"Conflict"}

type Registry added in v0.2.0

type Registry struct {
	// contains filtered or unexported fields
}

Registry maps type codes to the Kind values a gRPC client expects to receive. ReceiveGRPCError uses it so that an error sent as a Kind matches the client's copy of that Kind with Is.

A client that shares error definitions with its servers, for example through a common package, registers those kinds once at startup. A received kind is matched by type code, gRPC code, and category. The local Kind's definition supplies the HTTP code.

A registry is not needed to match built-in categories, or to keep an unregistered kind's type code, category, and HTTP code. A Registry is safe for concurrent use.

func NewRegistry added in v0.2.0

func NewRegistry(kinds ...*Kind) (*Registry, error)

NewRegistry returns a registry that contains kinds. It returns an error for a nil Kind or when two kinds share a type code.

Example

Register the kinds a client expects to receive, typically once at startup.

package main

import (
	"fmt"

	"github.com/stackus/errors"
)

func main() {
	ErrOrderNotFound := errors.NewKind("ORDER_NOT_FOUND", errors.ErrNotFound, "order not found")
	ErrOrderShipped := errors.NewKind("ORDER_SHIPPED", errors.ErrFailedPrecondition, "order already shipped")

	registry, err := errors.NewRegistry(ErrOrderNotFound, ErrOrderShipped)
	if err != nil {
		panic(err)
	}

	kind, ok := registry.Lookup("ORDER_SHIPPED")
	fmt.Println(ok, kind == ErrOrderShipped)
}
Output:
true true

func (*Registry) Lookup added in v0.2.0

func (r *Registry) Lookup(code string) (*Kind, bool)

Lookup returns the Kind registered for code. A nil Registry is safe to use and returns no match.

type TypeCoder added in v0.0.2

type TypeCoder interface {
	error
	TypeCode() string
}

TypeCoder is implemented by errors that report a type code.

Every error in this package implements TypeCoder, HTTPCoder, and GRPCCoder. Your own error types can implement any of them to be classified without wrapping. Codes a type does not provide use the UNKNOWN defaults. Because a non-nil error never reports success, a type code of "OK" is treated as "UNKNOWN", an HTTP code outside 400–599 as 500, and a gRPC code of OK as Unknown.

Directories

Path Synopsis
Package grpcerrs provides gRPC interceptors that send and receive classified errors from github.com/stackus/errors.
Package grpcerrs provides gRPC interceptors that send and receive classified errors from github.com/stackus/errors.

Jump to

Keyboard shortcuts

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