# Errors (https://v2.s3.dimah.dev/docs/server/errors)

Every failed request returns an HTTP status and a JSON body:

```json
{
  "code": "FILE_TOO_LARGE",
  "message": "File is 5242880 bytes; the limit is 2097152 bytes",
  "params": { "size": 5242880, "maxFileSize": 2097152 }
}
```

`code` is always set and stable. `message` is English, filled from `params`.
Show users your own text: map `code` and `params` to it, as
`useFormatDimahError` does.

## Throwing errors [#throwing-errors]

Throw `s3Error` with a catalog code to reject a call from a guard or hook.
Codes with `params` require them:

```ts
import { s3Error } from "@dimah-s3/server";

throw new Error("Not allowed"); // [!code --]
throw s3Error("UNAUTHORIZED"); // [!code ++]
throw s3Error("FORBIDDEN");
throw s3Error("OBJECT_NOT_FOUND");
throw s3Error("QUOTA_EXCEEDED", { limit: "bytes" });
throw s3Error("FILE_TOO_LARGE", { size: file.size, maxFileSize });
```

A plain `Error`, thrown anywhere, is logged, passed to `onError`, and returns
`500 INTERNAL_ERROR`. Its message never reaches the client.

For a code of your own, such as from a plugin, use `APIError.from`:

```ts
import { APIError } from "@dimah-s3/server";

throw APIError.from(402, {
  code: "PLAN_REQUIRED",
  message: "Upgrade your plan to upload videos",
});
```

## Handling errors [#handling-errors]

`createS3Client` and the React hooks reject with an `APIError`. Narrow it by
code with `isS3ErrorCode`:

```ts
import { isAPIError, isS3ErrorCode } from "@dimah-s3/core";

try {
  await api.download({ route: "avatar", key });
} catch (error) {
  if (isS3ErrorCode(error, "OBJECT_NOT_FOUND")) {
    // Handle the missing object
  } else if (isAPIError(error)) {
    console.log(error.statusCode, error.code, error.message, error.params);
  }
}
```

The client turns every failure into an `APIError`:

- A JSON error body keeps its `code`, `params`, and HTTP status. A `params`
  entry that is not a string or number is dropped; the rest of the error is
  kept.
- Any other error response becomes `INTERNAL_ERROR` (status 500 or above) or
  `UNKNOWN_ERROR`, with the real HTTP status.
- A `2xx` response whose body is not a JSON object or array, such as an SPA
  fallback `index.html`, becomes `UNKNOWN_ERROR` with its status.
- The browser upload transport raises `CHECKSUM_MISMATCH` when S3 rejects a
  checksummed PUT or part (`BadDigest` or `XAmzContentChecksumMismatch`).
- A request that fails before a response becomes `NETWORK_ERROR`.
- An aborted request rejects with the signal's abort reason, not an
  `APIError`.

`deleteMany` reports per-key failures as wire bodies in `failed[].error`.
Rebuild one with `APIError.fromShape(error)`; the status comes from the
catalog.

## Reporting errors [#reporting-errors]

`onError` receives every failure that is not an `APIError`: plain errors,
config errors, and S3 network errors. They are logged too, then answered with
`500 INTERNAL_ERROR` or `502 S3_NETWORK_ERROR`. Rejections you throw with
`s3Error` do not reach it. The context has the `request` and, when known,
the `operation`. It may be async: a rejection from it is logged, never
unhandled. An error from another library that is merely named `APIError` is
an unexpected failure too, so it is logged and reaches `onError`.

```ts title="lib/s3.ts"
export const s3 = dimahS3({
  onError: async (error, { request, operation }) => {
    Sentry.captureException(error, { extra: { url: request.url, operation } }); // [!code ++]
  },
  // ...
});
```

A throw from your `upload.onConfirmed`, even an `APIError`, returns
`500 INTERNAL_ERROR` and keeps the object, so the client can retry the
confirm. `onError` gets a wrapping `Error` with the original as `cause`.
Reject content in `confirmGuard` instead; a rejection there deletes the
object.

---

## Server codes [#server-codes]

Codes the server sends. The status is in `S3_ERROR_CODES[code].status`.

<ErrorCodeTable raisedBy="server" />

## Client codes [#client-codes]

Codes only the browser raises (`client: true` in the catalog). They never
come from the server.

<ErrorCodeTable raisedBy="client" />

## Frequently asked questions [#frequently-asked-questions]

<Accordions>

<Accordion title="How do I display friendly error messages on the client?">

Use `useFormatDimahError` from `@dimah-s3/react`. It maps each code and its
`params` to a translated string:

```tsx
import { useDownload, useFormatDimahError } from "@dimah-s3/react";

const formatError = useFormatDimahError();

const { download } = useDownload({
  route: "avatar",
  onError: (error) => toast.error(formatError(error)), // [!code ++]
});
```

</Accordion>

<Accordion title="Where do I find the message template for a code?">

`S3_ERROR_CODES` holds each entry's `status`, `message` template, and
`client` flag. `formatErrorMessage(template, params)` fills a template:

```ts
import { formatErrorMessage, S3_ERROR_CODES } from "@dimah-s3/core";

formatErrorMessage(S3_ERROR_CODES.UPLOAD_FAILED.message, { status: 403 });
// "Upload failed with HTTP 403"
```

</Accordion>

</Accordions>
