# Errors & validation (/docs/errors)



Every failure in the SDK — a client-side constraint violation, a failed PUT, an
aborted upload, a rejected route handler — arrives as a single error type:
`S3DeliveryError`. It carries a stable, machine-readable `code` you can branch on.

```ts
import { S3DeliveryError } from "s3delivery";

try {
  await uploadFiles(files, { maxFileSize: 5 * 1024 * 1024 });
} catch (e) {
  if (e instanceof S3DeliveryError && e.code === "FILE_TOO_LARGE") {
    toast.error("That file is too big (max 5 MB).");
  }
}
```

## `S3DeliveryError` [#s3deliveryerror]

```ts
class S3DeliveryError extends Error {
  readonly code: S3DeliveryErrorCode; // stable string, e.g. "FILE_TOO_LARGE"
  readonly status?: number; // HTTP status, when from a response
}
```

* **`code`** is the field to branch on — it is stable across messages and HTTP
  layers. The browser SDK re-throws errors from your [route handler](/docs/route-handler)
  with the same `code`, so client and server agree.
* **`status`** is present when the error came from an HTTP response (e.g. `413`
  for `FILE_TOO_LARGE` from the server).
* **`message`** is human-readable and may change — do not match on it.

The `code` type is an open string union, so server-defined codes surface verbatim,
but the SDK's own codes autocomplete.

## Error codes [#error-codes]

### Client-side validation (before any network request) [#client-side-validation-before-any-network-request]

| Code               | Thrown when                                      |
| ------------------ | ------------------------------------------------ |
| `TOO_MANY_FILES`   | More files than `maxFiles` were selected.        |
| `FILE_TOO_LARGE`   | A file exceeds `maxFileSize`.                    |
| `TYPE_NOT_ALLOWED` | A file's MIME type is not in `allowedFileTypes`. |
| `NO_FILES`         | An upload was attempted with an empty selection. |

### Transport / engine [#transport--engine]

| Code            | Thrown when                                                              |
| --------------- | ------------------------------------------------------------------------ |
| `UPLOAD_FAILED` | A storage PUT returned a non-2xx status.                                 |
| `NETWORK_ERROR` | The request failed at the network layer.                                 |
| `ABORTED`       | The upload was cancelled via an `AbortSignal` / `control.abort()`.       |
| `NO_ETAG`       | A multipart part response exposed no `ETag` (usually a CORS config gap). |

### Client / auth [#client--auth]

| Code              | Thrown when                                                   |
| ----------------- | ------------------------------------------------------------- |
| `MISSING_TOKEN`   | No API key was provided or found in the environment.          |
| `DOWNLOAD_FAILED` | `download()` could not fetch the signed URL.                  |
| `NO_BASE64`       | The runtime lacks a base64 decoder (used by `s3delivery/ai`). |
| `ERROR`           | A generic fallback when no more specific code applies.        |

### Server-defined [#server-defined]

The route handler and API may also surface their own codes verbatim, including:

| Code               | Status | Meaning                                                |
| ------------------ | ------ | ------------------------------------------------------ |
| `UNAUTHORIZED`     | `401`  | Missing or invalid API key.                            |
| `FILE_TOO_LARGE`   | `413`  | Over the route or plan single-file limit.              |
| `TYPE_NOT_ALLOWED` | `415`  | Content type rejected by route constraints.            |
| `QUOTA_EXCEEDED`   | `403`  | Upload would exceed your total storage quota.          |
| `NOT_FOUND`        | `404`  | File does not exist, is deleted, or you cannot see it. |
| `BAD_REQUEST`      | `400`  | Malformed input.                                       |

## Handling errors precisely [#handling-errors-precisely]

```ts
import { S3DeliveryError } from "s3delivery";

try {
  await upload();
} catch (e) {
  if (!(e instanceof S3DeliveryError)) throw e;
  switch (e.code) {
    case "FILE_TOO_LARGE":
    case "TYPE_NOT_ALLOWED":
    case "TOO_MANY_FILES":
      return showValidationError(e.message);
    case "ABORTED":
      return; // user cancelled — nothing to report
    case "QUOTA_EXCEEDED":
      return promptUpgrade();
    default:
      return reportUnexpected(e);
  }
}
```

## `validateFiles` [#validatefiles]

The standalone client-side constraint check that the browser uploader and React
hooks call internally. Pure and synchronous — safe to call anywhere, runs no I/O.
Use it to validate a selection before you commit to an upload (for example, to
disable a submit button).

```ts
import { validateFiles, S3DeliveryError } from "s3delivery";

function check(files: File[]) {
  try {
    validateFiles(files, {
      maxFiles: 10,
      maxFileSize: 25 * 1024 * 1024,
      allowedFileTypes: ["image/*", "application/pdf"],
    });
    return null;
  } catch (e) {
    return e instanceof S3DeliveryError ? e.message : "Invalid selection";
  }
}
```

It throws the **first** violation as an `S3DeliveryError` (`TOO_MANY_FILES` /
`FILE_TOO_LARGE` / `TYPE_NOT_ALLOWED`), or returns nothing if the selection is
valid.

### `UploadConstraints` [#uploadconstraints]

```ts
interface UploadConstraints {
  /** Max number of files in a single upload call. */
  maxFiles?: number;
  /** Max size, in bytes, for any single file. */
  maxFileSize?: number;
  /** Allowed MIME types: exact (`"application/pdf"`) or wildcard (`"image/*"`). */
  allowedFileTypes?: string[];
}
```

Every field is optional; omit one to leave that dimension unconstrained. Type
matching is case-insensitive, ignores a `; charset=…` suffix, and treats `"*"` /
`"*/*"` as allow-all. These same constraints (minus `maxFiles`) are enforced
server-side by the [route handler](/docs/route-handler#constraints).
