# Results and errors

> Every call returns a Result with a typed, serializable DbError.

Source: https://bettersupabase.com/docs/concepts/results

Repository methods never throw for database errors. They return an
`AsyncResult`, which you can await for a `Result` or unwrap:

```ts
const result = await db.customers.findById(id);
if (!result.ok) {
  if (result.error.kind === "not_found") return notFound();
  throw result.error;
}
result.data.name;

// Or throw a DbException on error:
const customer = await db.customers.findById(id).orThrow();

// Or chain:
const name = await db.customers
  .findById(id)
  .map((row) => row.name)
  .unwrapOr("Unknown");
```

`AsyncResult` has `orThrow`, `unwrapOr`, `map`, `mapError` and `andThen`.
`mapError` here rewrites the `DbError` inside one result; `betterSupabase.mapError()`
([below](#domain-errors-for-orthrow)) changes what `.orThrow()` throws.

## Error kinds [#error-kinds]

`DbError` is plain data, safe to return from server actions and RPC handlers.

| kind              | status | From                                                                                                                                                                                                                      |
| ----------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `not_found`       | 404    | `findById`, `update`, `delete` with no row, `PGRST116`, `P0002` (`no_data_found`)                                                                                                                                         |
| `unauthorized`    | 401    | Missing or expired JWT                                                                                                                                                                                                    |
| `forbidden`       | 403    | `42501`, RLS `WITH CHECK` failures; guards with `aal` set `required` ([MFA](/docs/auth/mfa-sso)); an [authorizer](/docs/extending/authorizers) refusal sets `permission`, and `approval` with `code: "APPROVAL_REQUIRED"` |
| `conflict`        | 409    | `23505` unique violation, with `constraint` and `columns`                                                                                                                                                                 |
| `foreign_key`     | 409    | `23503`                                                                                                                                                                                                                   |
| `check`           | 422    | `23514`, with `constraint`; a `jsonb-schemas` check is `validation` instead                                                                                                                                               |
| `not_null`        | 422    | `23502`, with `column`                                                                                                                                                                                                    |
| `exclusion`       | 409    | `23P01`                                                                                                                                                                                                                   |
| `invalid_input`   | 400    | `22*` data exceptions                                                                                                                                                                                                     |
| `invalid_value`   | 500    | A stored value the app type can't hold, such as `infinity` in a Temporal timestamp, with `column`                                                                                                                         |
| `raised`          | 400    | `RAISE EXCEPTION` (`P0001`); `hint` carries your app code                                                                                                                                                                 |
| `timeout`         | 504    | `57014` statement timeout, or a call past its `timeout` option                                                                                                                                                            |
| `serialization`   | 409    | `40001`, safe to retry                                                                                                                                                                                                    |
| `network`         | 503    | Fetch failures, `08*`                                                                                                                                                                                                     |
| `aborted`         | 499    | The `AbortSignal` fired                                                                                                                                                                                                   |
| `invalid_request` | 400    | A query the backend cannot express                                                                                                                                                                                        |
| `validation`      | 422    | Standard Schema issues from the validation plugin, or the [jsonb-schemas](/docs/blocks/sql#enforcing-jsonb-shapes) errors                                                                                                 |
| `multiple_rows`   | 409    | A single-row call matched several rows                                                                                                                                                                                    |
| `stale`           | 412    | `update(..., { expect })` found a newer row                                                                                                                                                                               |
| `max_affected`    | 400    | A write matched more rows than its `maxAffected`, `PGRST124`; `maxAffected` is the limit                                                                                                                                  |
| `rate_limited`    | 429    | Over a [write rate limit](/docs/blocks/sql#rate-limiting-writes); `retryAfter` is the seconds to wait                                                                                                                     |
| `quota_exceeded`  | 429    | A [usage quota](/docs/blocks/usage) is used up; `meter`, `limit` and `retryAfter` (seconds until the period resets)                                                                                                       |
| `unsupported`     | 501    | The executor can't run the operation, such as an include on [SQLite](/docs/repository/powersync)                                                                                                                          |
| `unexpected`      | 500    | Anything else                                                                                                                                                                                                             |

The `status` field feeds the [RFC 9457](/docs/standards) problem details
mapping in the server adapters.

## Custom kinds and mappers [#custom-kinds-and-mappers]

Add mappers for your own `RAISE` codes:

```ts
const betterSupabase = defineSupabase(schema, {
  errors: [
    (raw, fallback) =>
      raw.hint === "quota_exceeded"
        ? dbError("forbidden", fallback.message)
        : undefined,
  ],
});
```

Plugins can also contribute a `mapError`. These mappers turn a raw
Postgres or PostgREST error into a `DbError`; the next section turns a
`DbError` into your own error.

## Domain errors for `.orThrow()` [#domain-errors-for-orthrow]

`betterSupabase.mapError(fn)` sets the error `.orThrow()` throws for every `db` it
connects, instead of a `DbException`:

```ts
export class AppError extends Error {
  constructor(readonly error: DbError) {
    super(error.message, { cause: error });
  }
}

export const toAppError = (error: DbError) => new AppError(error);
export const betterSupabase = defineSupabase(schema).mapError(toAppError);

await db.customers.findById(id).orThrow(); // throws AppError
```

Results themselves keep their `DbError`, so they stay serializable. The
mapper survives `map`, `mapError` and `andThen`; a factory passed to
`.orThrow(factory)` takes precedence.

Set the `DbError` as the error's `cause`. The server, Next.js, Hono and
oRPC adapters then still answer with Problem Details, and `isConflict`,
`isCheck` and `isForeignKey` still match. TanStack Query hooks always see a
`DbException`, whatever the mapper.

## better-result [#better-result]

To return [better-result](https://github.com/dmmulroy/better-result) values
instead, wrap the `db` with `withBetterResult` or convert single results with
`toBetterResult`. See [better-result](/docs/concepts/better-result).