An error catalog is a fixed set of identified failures, each with a stable identifier and a page explaining it, rather than a collection of prose messages you are left to pattern-match. The practical difference is that your handler can branch on something that will not change under it.

Every failed Bird request returns the same envelope, nested under a top-level `error`. The HTTP status tells you the category, and the envelope explains what happened.

## Which field should I branch on?

Three of them are for machines and one is not, and mixing that up is the mistake this section exists to prevent.

**`type` is for coarse handling.** A closed enum that grows rarely: `validation_error`, `auth_error`, `permission_error`, `not_found_error`, `conflict_error`, `rate_limit_error`, `billing_error`, `internal_error` and a few more. This is what a switch statement should key on: retry a rate-limit error, surface a validation error to the user, page someone on an internal one.

**`code` is for specific handling.** An opaque, stable identifier like `E01001`. It is the canonical reference: unique, **never renamed and never reused**, and when an error is retired its code is permanently reserved. That guarantee is what makes it safe to match a handful of codes you treat specially.

**`message` is for people.** The wording changes without notice. Display it, log it, and never parse it. A handler that matches on message text breaks on a copy edit.

`name` is a readable slug that always accompanies the code rather than replacing it, and `doc_url` is a stable link to that code's page.

## How do I map codes to retries?

There is one rule and it is shorter than people expect: **retry `429` and `5xx`, and nothing else by default.**

Honour `Retry-After` on a rate limit, use exponential backoff on a server error, and send an idempotency key so a retried mutation cannot apply twice. Everything else is deterministic: a `422` will fail identically however many times you send it, and retrying a `401` just means being refused more often.

That is why branching on `type` rather than status alone is worth it. `rate_limit_error` and `internal_error` are the two retryable categories, and they are the two the type names for you.

## Why is every validation failure the same code?

Because a code per field-and-failure combination would be a catalog nobody could hold, and the information you need is one level down.

Every field-level validation failure is `E01001 ValidationError` with a `details` array, each entry a `param` and a `message`. So you branch once on the code and then read `details` to map problems onto form fields, using `param` as the key. The `message` strings inside `details` change like any other message, so display them rather than matching them.

## Do errors tell me how to fix them?

Some do, in two forms, and the second is the unusual one.

**`remediation`** is a human-readable next step, present when a recovery is known. A permission error that needs a scope your credential lacks comes back with a sentence saying to re-authenticate with a credential that has it. Show that as-is.

**`next`** lists the API operations that resolve the error, in the order to try them, and it is present for errors with a well-defined recovery such as an unmet precondition. Verify a domain, then retry the send. An agent or a CLI can execute that list directly rather than parsing advice out of prose, which is the part worth knowing if you are building something that recovers on its own.

There is also `vendor_code`, present only when Bird is surfacing a downstream system's own code, an SMTP response or a payment decline, that you may want to act on separately.

## What should my handler do about codes it has never seen?

Have a default branch, because **new codes ship regularly** as products grow and the `type` enum occasionally gains a value.

An exhaustive match over either field is a handler that breaks on a release you did not read about. Treat an unrecognised type as its nearest category or as an unknown failure, and log the code so you find out what it was.

One logging habit is worth the line it costs: **log `code`, `name` and `request_id` together.** The code is what you will search for in the docs and in your own history; the request ID is what support needs to trace the exact call.

[Errors](/docs/guides/errors) has the full envelope with real captured responses, and the [error reference](/docs/api/errors) is the catalog itself, one page per code.