> ## Documentation Index
> Fetch the complete documentation index at: https://docs.pulseshiga.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Error handling

> The typed errors @pulsebyshiga/node throws, and an optional pattern for mapping them into your own error layer.

Unlike SDKs that return an `{ data, error }` tuple, `@pulsebyshiga/node` returns
the value directly and throws. Catch these:

| Error                        | When                                                       | Useful fields                            |
| ---------------------------- | ---------------------------------------------------------- | ---------------------------------------- |
| `PulseApiError`              | The engine responded with a non-2xx.                       | `status`, `code`, `message`, `requestId` |
| `PulseConnectionError`       | Network failure / timeout, after the built-in retries.     | `message`                                |
| `PulseAccessError`           | SDK misuse (e.g. an API key constructed in a browser).     | `message`                                |
| `SignatureVerificationError` | `webhooks.verify()` — bad signature, replay, or malformed. | `message`                                |

All extend `PulseError`, so `catch (e) { if (e instanceof PulseError) … }`
catches everything from the SDK:

```ts theme={null}
try {
  const session = await pulse.collectionSessions.create(payload);
  return session;
} catch (error) {
  if (error instanceof PulseApiError) {
    logger.error('Pulse rejected the session', {
      status: error.status,
      code: error.code,
      requestId: error.requestId,
    });
  } else if (error instanceof PulseConnectionError) {
    logger.error('Could not reach Pulse', { message: error.message });
  }
  throw error; // re-throw, or map into your own error layer — see below
}
```

## Optional: map into your app's error layer

If your backend centralizes errors (e.g. behind your own `ErrorFactory`), map
the SDK's typed errors the same way you wrap any vendor — keeping the raw
vendor detail in metadata and logs, never in the customer-facing message.
`ErrorFactory` and `logger` below are **your own** utilities, not part of the
SDK — the SDK's only contract is the four typed errors above.

```ts theme={null}
} catch (error) {
  logger.error('Pulse collectionSessions.create failed');

  if (error instanceof PulseApiError) {
    throw ErrorFactory.externalApi.error('Pulse', error.status, {
      endpoint: 'collectionSessions.create',
      externalApiResponse: {
        code: error.code,
        message: error.message,
        requestId: error.requestId,
      },
    });
  }

  throw ErrorFactory.fromError(error, 'EXTERNAL_API_ERROR', {
    externalApiName: 'Pulse',
    endpoint: 'collectionSessions.create',
  });
}
```

## Error codes

`PulseApiError.code` is a string set by the API — it isn't a frozen enum yet. The values
below are the ones you'll encounter in today's sandbox; branch on the codes you handle
and treat any unrecognised code defensively (fall back on `error.status` and
`error.message`).

| `code`                  | HTTP | Meaning                                              |
| ----------------------- | ---- | ---------------------------------------------------- |
| `unauthorized`          | 401  | Missing or invalid API key / session token           |
| `session_not_found`     | 401  | The `cs_` session token is unknown or expired        |
| `order_not_found`       | 404  | No order with that id                                |
| `not_found`             | 404  | Resource not found                                   |
| `invalid_request`       | 400  | Malformed request body                               |
| `invalid_amount`        | 400  | Amount missing or not greater than zero              |
| `invalid_quote`         | 400  | Quote is missing, mismatched, or has expired         |
| `invalid_gate`          | 400  | Unknown or unsupported gate                          |
| `missing_user_ref`      | 400  | Naira gate requires a `user_ref`                     |
| `invalid_account_block` | 400  | Naira `account` block is missing or malformed        |
| `invalid_selection`     | 400  | Selected asset/network isn't offered on this order   |
| `not_selectable`        | 409  | Order is past the selection stage                    |
| `resolve_failed`        | 400  | Bank-account resolution failed                       |
| `network_not_enabled`   | 400  | Network disabled for this partner (dashboard config) |
| `asset_not_enabled`     | 400  | Asset disabled for this partner                      |
| `no_deposit`            | 409  | Action needs a deposit that hasn't arrived           |
| `invalid_event`         | 400  | Unknown sandbox simulate event                       |
| `internal`              | 500  | Unexpected server error (retried automatically)      |
| `unknown_error`         | —    | SDK fallback when the response carries no `code`     |
