# Errors

> One error class, typed on the client from what resolvers return, with or without declarations.

There is one error class, `TrpcError`. Return `error({ code })` from a resolver or a middleware, or fail with it in Effect code (`Effect.fail(error(…))`, `yield* error(…)`). The procedure's error union is inferred from those, with no declaration:

```ts
import { error } from '@trpc12/trpc/server';
import { z } from 'zod';

const byId = t.procedure
  .input(z.object({ id: z.string() }))
  .query(
    (opts) =>
      db.post.find(opts.input.id) ??
      error({ code: 'NOT_FOUND', data: { id: opts.input.id } }),
  );
```

## Declaring errors

Declaring errors with `.errors()` is optional. It adds typed constructors and `data` schemas. It also types what inference can't see: errors thrown from helpers, since a `throw` has no type.

```ts
import { z } from 'zod';

const byId = t.procedure
  .errors({
    NOT_FOUND: { data: z.object({ id: z.string() }) },
    GONE: { status: 410, message: 'This post was removed' },
  })
  .input(z.object({ id: z.string() }))
  .query(
    (opts) =>
      db.post.find(opts.input.id) ??
      opts.errors.NOT_FOUND({ data: { id: opts.input.id } }),
  );
```

## On the client

On the client, the errors in the union are `defined: true`, with their `data`, whether they were returned, failed or declared. Anything else arrives as a built-in error (`defined: false`), and it never has `data`:

- An undeclared thrown error with a built-in code (`throw new TrpcError({ code: 'CONFLICT', data })`) keeps its code and loses its `data`.
- An undeclared thrown error with a custom code, or a crash, is an `INTERNAL_SERVER_ERROR`. Its message is masked unless `NODE_ENV` is `development` or `test`, or `exposeUnexpectedErrors: true`.

So the client's union only has codes it can handle, and `onError` still sees the original error. Declare a thrown error (or return it) to give it a custom code or `data` on the client. There is no `errorFormatter`.

```ts
import { matchError, safe } from '@trpc12/trpc/client';

const [post, err] = await safe(client.post.byId.query({ id: '1' }));
if (err) {
  matchError(err, {
    NOT_FOUND: (e) => console.log(e.data),
    // (property) TrpcError<"NOT_FOUND", { id: string; }>.data: {
    //     id: string;
    // }
    BAD_REQUEST: (e) => console.log(e.data.issues),
    builtin: (e) => console.log(e.code),
  });
}
```

See [Errors without try/catch](https://v12.trpc.io/v12/docs/client/errors.md) for `safe()`, `matchError()` and the built-in codes.

## Codes and statuses

Any string is a code. The built-in ones (`NOT_FOUND`, `UNAUTHORIZED`, …) have their HTTP status; a custom code is a 400 unless its declaration (`.errors()` or a `TrpcError.Tagged` class) has a `status`. The status is for HTTP: errors don't carry it, and neither do error bodies or socket frames.

```ts
t.procedure
  .errors({ OUT_OF_STOCK: { status: 409 } })
  .mutation(() => error({ code: 'OUT_OF_STOCK' })); // 409

t.procedure.mutation(() => error({ code: 'OUT_OF_STOCK' })); // 400
```

## In Effect code

An error's `_tag` is its code, so Effect code narrows with `catchTag`:

```ts
Effect.catchTag('NOT_FOUND', () => Effect.succeed(null));
```

A failure that isn't a `TrpcError` is a type error until it is mapped, so internals never reach clients. `TrpcError.Tagged` classes mark the errors clients may see ([Effect](https://v12.trpc.io/v12/docs/server/effect.md#errors-clients-may-see)).

## Credits

Declared errors with `.errors()` follow [oRPC's typed errors](https://orpc.dev/docs/error-handling).
