Help us shape the future of Go! Take the 2026 Go Developer Survey

rigerr

package
v0.6.7 Latest Latest
Warning

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

Go to latest
Published: Oct 3, 2026 License: MIT Imports: 4 Imported by: 0

Documentation

Overview

Package rigerr is the error vocabulary shared by generated code.

Every failure carries a machine-readable code as well as a message. Status codes alone are too coarse — three unrelated failures all return 400 — and a message alone leaves a client parsing prose to decide whether to retry.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func Aborted added in v0.6.0

func Aborted(err error) bool

Aborted reports whether err is a caller that went away.

Nothing in rig ever cancels a request context. There is no BaseContext on the server it builds, and shutdown waits for the requests in flight rather than cancelling them — so a context.Canceled arriving at a handler boundary is the caller hanging up, and nothing else. A closed browser tab, not a failure.

It deliberately has no Code. A status would have to be a non-standard one, which would reach the generated OpenAPI document and every client's Is helpers, and an abandoned request does not need a status: it needs no response, because there is nobody left to read one.

The assumption it rests on is rig's, not Go's. A handler that cancels a context of its own and returns that error is telling this function the caller left when it did not, and would have its answer dropped.

Example

A caller that hung up is told apart from a server that broke, which the code alone cannot do: there is deliberately no code for it, because an abandoned request needs no status — it needs no response.

package main

import (
	"context"
	"fmt"

	"github.com/simonjanss/rig/runtime/rigerr"
)

func main() {
	err := rigerr.Internal(context.Canceled, "listing todos")

	fmt.Println(rigerr.Aborted(err), rigerr.TimedOut(err))
	fmt.Println(rigerr.CodeOf(err))

}
Output:
true false
Internal

func FieldsOf

func FieldsOf(err error) (any, bool)

FieldsOf returns the per-field detail an error carries, if it carries any.

func Is

func Is(err error, code Code) bool

Is reports whether an error carries the given code.

func StatusOf

func StatusOf(err error) int

StatusOf returns the HTTP status for an error.

func TimedOut added in v0.6.0

func TimedOut(err error) bool

TimedOut reports whether err is something the server waited on that did not answer.

The mirror image of Aborted rather than a sibling of it, and the reason the two are not one predicate. Nothing in rig's request path puts a deadline on a request context either — the read and write timeouts on the server are connection deadlines and surface as write errors — so a context.DeadlineExceeded here was set below the handler by something the server was waiting on. Nobody hung up; something did not answer.

Unlike Aborted this does carry a Code: CodeUnavailable, through CodeOf. There is a caller waiting for an answer, and 503 is a more useful one than 500 to a client deciding whether to retry.

Example

A timeout is the mirror image of a cancelled caller rather than a sibling of it. Nobody hung up; something the server was waiting on did not answer, and there is still a caller to tell.

package main

import (
	"context"
	"fmt"

	"github.com/simonjanss/rig/runtime/rigerr"
)

func main() {
	err := fmt.Errorf("acquire a connection: %w", context.DeadlineExceeded)

	fmt.Println(rigerr.TimedOut(err), rigerr.Aborted(err))
	fmt.Println(rigerr.CodeOf(err), rigerr.StatusOf(err))

}
Output:
true false
Unavailable 503

func Wrap

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

Wrap adds context to an error, keeping whatever code it already carried.

An error with no code becomes Internal, which is the honest answer for one that never passed through this package: nobody decided what it means to a client, so it is a server problem until somebody does. An error that does carry a code keeps it — a rule that refused with a Conflict is still a conflict once the layer above has said which rule it was.

Types

type Answer added in v0.6.0

type Answer struct {
	// Code and Status are the same fact twice, because a caller assembling a
	// response wants both and deriving one from the other at four call sites is
	// how they drift.
	Code   Code
	Status int
	// Message is already redacted.
	Message string
	// Fields is nil unless the failure carried per-field detail.
	Fields any
}

Answer is what an error means on the wire, before anything is encoded.

Separate from any envelope so that callers with different ones — the generated server, whose field names go through `api.json_case`, httpx's JSON envelope, and the text/plain page a provider callback answers with — can share the decision without sharing the shape. That is the whole seam between the two: the classification is one implementation, the encoding is several.

func AnswerFor added in v0.6.0

func AnswerFor(err error) Answer

AnswerFor classifies err: the code it carries, the status that code is answered with, the message a client may be shown, and the per-field detail if there is any.

The one thing a caller must not have to remember: **an internal failure's detail never reaches the client**. It is exactly the kind of thing that leaks a table name, a constraint, or a connection string, so the message becomes a fixed sentence and the request id becomes the only way to find out more.

It is here rather than beside the HTTP envelope because it needs nothing from net/http, and a package that only wants to classify an error should not have to take a dependency on everything answering one implies. httpx.AnswerFor is this plus the one header a 429 has to leave with, and is what a route that has a ResponseWriter in hand should call.

type Code

type Code string

Code is the machine-readable reason a request failed. The set is closed: a handler cannot invent a status a client has not been told about.

const (
	CodeBadRequest          Code = "BadRequest"
	CodeUnauthorized        Code = "Unauthorized"
	CodeForbidden           Code = "Forbidden"
	CodeNotFound            Code = "NotFound"
	CodeConflict            Code = "Conflict"
	CodeUnprocessableEntity Code = "UnprocessableEntity"
	CodeRateLimited         Code = "RateLimited"
	// CodeTooLarge reports a request body past the limit the endpoint accepts.
	//
	// It is separate from a bad request because the caller's request was
	// well-formed and the answer is a number: send fewer bytes. A 400 that also
	// means a malformed body cannot say that.
	CodeTooLarge Code = "TooLarge"
	// CodeUnsupportedMediaType reports a body whose content type the endpoint
	// does not accept — a JSON-only endpoint sent a form, or an upload of a type
	// the project does not allow.
	CodeUnsupportedMediaType Code = "UnsupportedMediaType"
	// CodeUpgradeRequired reports that the caller was built against an API
	// revision the server no longer serves.
	//
	// It is its own code rather than a bad request because it is the one failure
	// a client can actually do something about, and "regenerate your client" is
	// not advice anybody can take from a 400 that also means a malformed body.
	CodeUpgradeRequired Code = "UpgradeRequired"
	// CodeUnavailable reports that something the server was waiting on did not
	// answer in time.
	//
	// It is separate from an internal failure because nothing here is broken: the
	// path works and was too slow, which is the difference between a caller that
	// should retry and one that should not. A 500 tells it neither. It is what a
	// [context.DeadlineExceeded] becomes, and nothing in rig's own request path
	// sets a deadline on a request context — so one arriving here was set below
	// the handler, by a pool acquire, an upstream client, or the application's own
	// timeout around a query.
	CodeUnavailable Code = "Unavailable"
	CodeInternal    Code = "Internal"
)

The codes. Each one maps to a status through Code.HTTPStatus, and adding one here without a case there is what the default arm is for — an unmapped code is a 500 rather than a compile error, so the two are kept in step by reading them together.

func CodeOf

func CodeOf(err error) Code

CodeOf returns the code an error carries, or Internal when it carries none.

An error that never passed through this package is a bug rather than a client mistake, and reporting it as anything other than a server error would be telling the caller to fix something that is not theirs.

Example

The code survives wrapping, which is what makes it safe for a service to add context on the way out. A handler reads the code off whatever it was handed, however many layers added to it.

package main

import (
	"errors"
	"fmt"

	"github.com/simonjanss/rig/runtime/rigerr"
)

func main() {
	err := rigerr.Conflict("that slug is taken")
	wrapped := fmt.Errorf("create the project: %w", err)

	fmt.Println(rigerr.CodeOf(wrapped), rigerr.StatusOf(wrapped))
	fmt.Println(rigerr.Is(wrapped, rigerr.CodeConflict))
	fmt.Println(errors.Is(wrapped, err))

}
Output:
Conflict 409
true
true
Example (Uncoded)

An error that never passed through this package is a server problem until somebody decides otherwise, so it reads as Internal rather than as something the caller could fix.

package main

import (
	"errors"
	"fmt"

	"github.com/simonjanss/rig/runtime/rigerr"
)

func main() {
	fmt.Println(rigerr.CodeOf(errors.New("the connection went away")))
	fmt.Println(rigerr.StatusOf(errors.New("the connection went away")))

}
Output:
Internal
500

func (Code) HTTPStatus

func (c Code) HTTPStatus() int

HTTPStatus maps a code to its status.

type Coder

type Coder interface {
	error
	ErrorCode() Code
}

Coder is an error that names its own code.

It exists for a failure that is more than a message — the generated per-input validation errors implement it — so such an error can be returned as itself rather than flattened into an Error carrying prose.

type Error

type Error struct {
	Code    Code
	Message string
	// Err is the underlying cause, kept for logs and errors.Is. It is never
	// shown to a client: an internal failure's detail is exactly the kind of
	// thing that leaks a table name or a connection string.
	Err error
}

Error is a failure with a code.

func BadRequest

func BadRequest(format string, args ...any) *Error

BadRequest reports a malformed request or an unparseable parameter.

func Conflict

func Conflict(format string, args ...any) *Error

Conflict reports that the request contradicts the current state.

func Forbidden

func Forbidden(format string, args ...any) *Error

Forbidden reports that the caller is known but not permitted.

func Internal

func Internal(err error, format string, args ...any) *Error

Internal reports a server-side failure, keeping the cause for the logs.

Example

Internal keeps the cause for the log and shows the caller only the message. The detail of an internal failure is exactly the kind of thing that leaks a table name or a connection string.

package main

import (
	"errors"
	"fmt"

	"github.com/simonjanss/rig/runtime/rigerr"
)

func main() {
	cause := errors.New("dial tcp 10.0.0.4:5432: connect: connection refused")
	err := rigerr.Internal(cause, "load the todo list")

	fmt.Println(err)                   // what a log gets
	fmt.Println(err.Code, err.Message) // what a client gets
	fmt.Println(errors.Is(err, cause)) // the cause is still reachable

}
Output:
Internal: load the todo list: dial tcp 10.0.0.4:5432: connect: connection refused
Internal load the todo list
true

func Invalid

func Invalid(format string, args ...any) *Error

Invalid reports a well-formed request that failed validation.

func NotFound

func NotFound(format string, args ...any) *Error

NotFound reports that no such resource exists, or that it belongs to another tenant. The two are deliberately indistinguishable: answering 403 for a row in someone else's tenant confirms the row exists.

Example

A service returns a coded error; the generated handler asks it for a status and a code without knowing which rule produced it.

package main

import (
	"fmt"

	"github.com/simonjanss/rig/runtime/rigerr"
)

func main() {
	err := rigerr.NotFound("no todo with id %q", "t-42")

	fmt.Println(err)
	fmt.Println(rigerr.CodeOf(err), rigerr.StatusOf(err))

}
Output:
NotFound: no todo with id "t-42"
NotFound 404

func RateLimited

func RateLimited(format string, args ...any) *Error

RateLimited reports that the caller should slow down.

func TooLarge

func TooLarge(format string, args ...any) *Error

TooLarge reports a body past the limit the endpoint accepts.

func Unauthorized

func Unauthorized(format string, args ...any) *Error

Unauthorized reports that no valid session or key was presented.

func Unavailable added in v0.6.0

func Unavailable(err error, format string, args ...any) *Error

Unavailable reports that something the server depends on did not answer in time, keeping the cause for the logs.

func UnsupportedMediaType

func UnsupportedMediaType(format string, args ...any) *Error

UnsupportedMediaType reports a body whose content type the endpoint refuses.

func UpgradeRequired

func UpgradeRequired(format string, args ...any) *Error

UpgradeRequired reports that the caller is older than the server will serve.

func (*Error) Error

func (e *Error) Error() string

Error is the code, the message, and the cause — everything, because this is for a log. What a client sees is assembled from Error.Code and Error.Message alone.

func (*Error) HTTPStatus

func (e *Error) HTTPStatus() int

HTTPStatus is the status this error should be returned as.

func (*Error) Unwrap

func (e *Error) Unwrap() error

Unwrap returns the underlying cause, so errors.Is and errors.As reach it.

func (*Error) Wrap

func (e *Error) Wrap(err error) *Error

Wrap attaches a cause to an error built by one of the constructors above.

type FieldCode

type FieldCode string

FieldCode says what kind of thing was wrong with one field, for a client that has to decide something rather than display something.

The set is fixed and shared by every project, so a client switches on it once. What varies goes in the message.

const (
	// FieldCodeCannotBeEmpty means the field is required and was left blank.
	FieldCodeCannotBeEmpty FieldCode = "CannotBeEmpty"
	// FieldCodeCannotBeNull means the field was cleared and the column cannot
	// hold null.
	FieldCodeCannotBeNull FieldCode = "CannotBeNull"
	// FieldCodeTooLong means longer than the column allows.
	FieldCodeTooLong FieldCode = "TooLong"
	// FieldCodeTooShort means shorter than the rule allows.
	FieldCodeTooShort FieldCode = "TooShort"
	// FieldCodeOutOfRange means outside the allowed range.
	FieldCodeOutOfRange FieldCode = "OutOfRange"
	// FieldCodeInvalidValue means not one of the values this field accepts.
	FieldCodeInvalidValue FieldCode = "InvalidValue"
	// FieldCodeAlreadyExists means another row has this value and it has to be
	// unique.
	FieldCodeAlreadyExists FieldCode = "AlreadyExists"
	// FieldCodeNotFound means it names something that does not exist.
	FieldCodeNotFound FieldCode = "NotFound"
	// FieldCodeNotAllowed means a legal value, but not one this caller may
	// set, or not from the state the row is in.
	FieldCodeNotAllowed FieldCode = "NotAllowed"
)

The kinds of wrong a field can be.

type FieldError

type FieldError struct {
	Code    FieldCode `json:"code"`
	Message string    `json:"message"`
}

FieldError is one thing wrong with one field.

It does not name the field. It is reached through the member of an input's error struct that stands for that field, so the name is where the value is, and there is no second copy of it to disagree.

It lives here rather than being generated per project because none of it is per project: the same nine codes and the same two members, so one definition serves every model and every client.

func AsFieldError

func AsFieldError(err error) (*FieldError, bool)

AsFieldError returns the field error an error carries, and whether it was one at all.

It is what separates a rule that failed from a rule that could not be run: the first is about the caller's input and belongs in a 422 under the field it names, the second is about the server and belongs nowhere near it.

Named As rather than Is because it hands back the value, the way errors.As does.

func NewFieldError

func NewFieldError(code FieldCode, format string, args ...any) *FieldError

NewFieldError builds one. A rule with nothing better to say than "no" uses FieldCodeInvalidValue.

func (*FieldError) Error

func (e *FieldError) Error() string

Error implements error, so a validation rule can return one directly.

func (*FieldError) ErrorCode

func (e *FieldError) ErrorCode() Code

ErrorCode implements Coder, and says Internal on purpose.

A field error means nothing on its own: it says what was wrong without saying what it was wrong about. Reaching the HTTP layer as the whole answer means one was returned from somewhere that has no field to attach it to — a service method rather than a validation rule — and the client cannot act on "CannotBeEmpty" with nothing to hang it off.

So it is a 500 with the generic message, which is what any other bug is. The alternative, a 422 with an empty body, would blame the caller for a mistake in the service.

type FieldReporter

type FieldReporter interface {
	error
	ErrorFields() any
}

FieldReporter is an error that can say what was wrong with each field.

ErrorFields returns a value shaped like the input it describes: one member per field of the request, each holding the problem with that field or nothing. A client can then attach every message to the control the person is looking at, instead of parsing one sentence for field names.

Jump to

Keyboard shortcuts

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