# Errors (/docs/v/0.6.1/reference/errors)



Every error the SDK throws is an `EmailSdkError` (or subclass) with a machine-readable `code` and a `retryable` flag. Three subclasses narrow the common cases:

```ts
import {
  EmailSdkError, // base class
  EmailProviderError, // a provider call failed
  EmailValidationError, // bad message or client config
  EmailProviderNotFoundError, // unregistered routing name
  isRetryableEmailError,
} from "@opencoredev/email-sdk";
```

## Properties [#properties]

All four classes carry the same fields (plus the standard `Error` `message` and `cause`):

<TypeTable
  type="{
  code: {
    description: &#x22;Machine-readable error code. Branch on this, not on message text.&#x22;,
    type: &#x22;string&#x22;,
    required: true,
  },
  retryable: {
    description: &#x22;Whether retrying the same adapter is reasonable.&#x22;,
    type: &#x22;boolean&#x22;,
    default: &#x22;false&#x22;,
    required: true,
  },
  provider: {
    description: &#x22;Routing name of the adapter that failed, when known.&#x22;,
    type: &#x22;string&#x22;,
  },
  status: {
    description: &#x22;HTTP status from the provider, when there was one.&#x22;,
    type: &#x22;number&#x22;,
  },
  details: {
    description: &#x22;Parsed provider error body, validation context, or collected failures.&#x22;,
    type: &#x22;unknown&#x22;,
  },
}"
/>

## Classes and codes [#classes-and-codes]

| Class                        | Code                   | Thrown when                                                                                                                                                                                                                      |
| ---------------------------- | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `EmailValidationError`       | `validation_error`     | The message fails [validation](/docs/v/0.6.1/reference/message#validation), an adapter rejects [unsupported fields](/docs/v/0.6.1/adapters/field-support), or client config is invalid (duplicate adapter/plugin, no adapters). Never retryable. |
| `EmailProviderNotFoundError` | `provider_not_found`   | A routing name is not registered — `Email provider "x" is not registered.` Never retryable.                                                                                                                                      |
| `EmailProviderError`         | `provider_error`       | A provider request fails: non-2xx response or network error. `status` and `details` carry the provider's answer.                                                                                                                 |
| `EmailSdkError`              | `all_providers_failed` | Two or more routes were attempted and all failed. `details` holds one error per adapter, in route order.                                                                                                                         |
| `EmailSdkError`              | `retry_loop_exited`    | Internal guard for an impossible retry-loop exit. Treat as a bug if seen.                                                                                                                                                        |

When only **one** route was attempted, its error is rethrown as-is — `all_providers_failed` only appears after a multi-adapter route exhausts every option. Errors adapters throw that are not already `EmailProviderError` are wrapped into one, keeping the original as `cause` (or in `details` for non-`Error` values).

## What counts as retryable [#what-counts-as-retryable]

`retryable: true` is set for failures that may succeed on a re-attempt:

* HTTP `408`, `409`, `425`, `429`, and any `5xx`
* Network failures: `ECONNRESET`-family error codes, `fetch failed`, socket and timeout errors

Everything else is non-retryable: `4xx` like `401`/`422`, validation errors, unknown routing names, and aborted requests (`AbortError`). This classification drives the default retry policy — see [fallbacks and retries](/docs/v/0.6.1/concepts/fallbacks-and-retries#what-retries-by-default).

## `isRetryableEmailError(error)` [#isretryableemailerrorerror]

```ts
isRetryableEmailError(error: unknown): boolean
```

Returns `true` only for an `EmailSdkError` with `retryable: true`; any other value returns `false`. It is the default `shouldRetry` predicate, and safe to call on anything a `catch` block hands you.

## Handling errors in app code [#handling-errors-in-app-code]

Branch on class first, then on `code` and `retryable`:

```ts
import {
  EmailProviderError,
  EmailSdkError,
  EmailValidationError,
} from "@opencoredev/email-sdk";

try {
  await email.send(message);
} catch (error) {
  if (error instanceof EmailValidationError) {
    // Bad message shape or unsupported field — fix the code path, do not retry.
    throw error;
  }

  if (error instanceof EmailProviderError) {
    console.error(`${error.provider} failed`, error.status, error.details);
    if (error.retryable) {
      await queue.retryLater(message); // transient — re-enqueue
      return;
    }
    throw error; // 401/422-style failure: needs a config or account fix
  }

  if (error instanceof EmailSdkError && error.code === "all_providers_failed") {
    // Every route failed; details is one error per adapter, in route order.
    console.error(error.details);
  }

  throw error;
}
```

Inside a send, you rarely handle retries yourself — configure the client's [`retry` and `fallback`](/docs/v/0.6.1/concepts/fallbacks-and-retries) options and let the SDK exhaust them before anything reaches your `catch`.
