# Outcomes and errors (https://v2.s3.dimah.dev/docs/react/guides/outcomes)

Hook actions do not throw for transfer errors. Each one resolves with an
**outcome** for one key, or a **result** for a batch. Read the status, then
the data or the error.

## Outcomes [#outcomes]

`remove`, `download`, and `presign` resolve with an outcome:

```ts
type OperationOutcome<TData> =
  | { status: "success"; key: string; data: TData }
  | { status: "error"; key: string; error: APIError }
  | { status: "blocked"; key: string }
  | { status: "aborted"; key: string };
```

- **`success`** — `data` holds the response.
- **`error`** — `error` is an `APIError`.
- **`blocked`** — a `before*` callback returned `false`.
- **`aborted`** — the call was cancelled. A delete has no cancel, so it never ends this way.

```ts
const outcome = await download(objectKey);

if (outcome.status === "error") {
  console.log(outcome.key, outcome.error.code);
}
```

## Results [#results]

Batch actions resolve with one result that sorts every unit:

| Action                      | Result                                           |
| --------------------------- | ------------------------------------------------ |
| `useUpload().handleFiles`   | `{ uploaded, failed, blocked, aborted }` by file |
| `useDelete().removeMany`    | `{ deleted, failed, blocked }` by key            |
| `useDownload().presignMany` | `{ signed, failed, blocked }` by key             |

`failed` entries pair the unit with its `APIError`, such as
`{ key, error }`.

## Joined calls [#joined-calls]

The download and delete hooks track each key on its own. A second call for a
key that is already running does not start a new request. It joins the
running call and resolves with the same outcome, and the callbacks do not run
a second time.

## Callback order [#callback-order]

Every hook runs its callbacks the same way:

1. **`before*`** (`beforeUpload`, `beforeDownload`, `beforeDelete`) runs once per call. Returning `false` blocks the call. A throw fails it with `HOOK_FAILED`.
2. **`on*Start`** (`onUploadStart`, `onDeleteStart`, `onDownloadStart`) runs once per call. A throw fails it with `HOOK_FAILED`.
3. **`onError`** runs once per failed unit, then **`onSuccess`** once per successful unit.
4. **`onSettled`** runs once per call, last.

`onSuccess`, `onSettled`, and the other after-hooks are awaited. If one throws,
the error goes to the global `reportError` and the outcome does not change.

## Errors [#errors]

Every error is an `APIError` from `@dimah-s3/core`:

- **`code`** — always set. Branch on it with `isS3ErrorCode(error, code)`.
- **`params`** — values for the message, such as `{ size, maxFileSize }`.
- **`statusCode`** — the HTTP status.
- **`message`** — English. Show users `useFormatDimahError()` instead; see [Helpers](https://v2.s3.dimah.dev/docs/react/helpers#errors).

Server codes come from the route and your hooks. Some codes are raised in the
browser only:

| Code              | When                                                         |
| ----------------- | ------------------------------------------------------------ |
| `TOO_MANY_FILES`  | A call has more valid files than `maxFiles`.                 |
| `HOOK_FAILED`     | A `before*` or `on*Start` callback threw.                    |
| `NETWORK_ERROR`   | A request got no response.                                   |
| `UPLOAD_FAILED`   | S3 rejected an upload. `params.status` is S3's status.       |
| `DOWNLOAD_FAILED` | A fetch download got a non-2xx response.                     |
| `PARTIAL_FAILURE` | `upload.error` when more than one file failed.               |
| `UNKNOWN_ERROR`   | Anything that is not an `APIError`. The original is `cause`. |

The full table is in [Errors](https://v2.s3.dimah.dev/docs/server/errors).

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

const { failed } = await upload.handleFiles(files);
for (const { file, error } of failed) {
  if (isS3ErrorCode(error, "FILE_TOO_LARGE")) {
    console.log(`${file.name} is too large`);
  }
}
```

## Aborts [#aborts]

A cancel is not an error. Hooks report it as an `aborted` outcome, or as the
`aborted` list of an upload result.

Engine functions such as `uploadFile` reject with the signal's reason instead.
The upload abort reasons are `DOMException`s named `AbortError`. Read which
kind with `getUploadAbortKind`:

```ts
import { getUploadAbortKind } from "@dimah-s3/react/engine";

try {
  await uploadFile(api, file, options);
} catch (error) {
  const kind = getUploadAbortKind(error); // "cancel", "detach", or null
  if (kind === null) throw error;
}
```

See [Engine](https://v2.s3.dimah.dev/docs/react/guides/engine).

## Type reference [#type-reference]

<AutoTypeTable path="packages/react/src/types/operation.ts" name="KeyError" />

<AutoTypeTable
  path="packages/react/src/types/operation.ts"
  name="TransferProgress"
/>
