---
title: Cancellation
description: Cancellation through AbortSignal, checked before, between, and during retries.
type: package
package: "@zap-studio/retry"
---

`AbortSignal` cancels retries, checked before, between, and during attempts. `runRetryPolicy(...)` accepts an `AbortSignal` through the `signal` option. Use it to stop retrying on user navigation, request timeouts, shutdown, or parent workflow cancellation.

## Basic Usage

Pass `controller.signal` to `runRetryPolicy(...)`; aborting the controller stops the run with an `AbortError`.

```ts
import { AbortError, exponentialBackoff, runRetryPolicy } from "@zap-studio/retry";

const controller = new AbortController();

const policy = exponentialBackoff({
  maxAttempts: 5,
  baseDelayMs: 100,
  maxDelayMs: 2_000,
});

const promise = runRetryPolicy(
  policy,
  async () => {
    return await doWork();
  },
  { signal: controller.signal },
);

controller.abort(new Error("Request canceled by user"));

try {
  await promise;
} catch (error) {
  if (error instanceof AbortError) {
    console.error("Retry aborted:", error.message);
  }
}
```

## Throw Mode

In the default throw mode, an aborted run rejects with `AbortError`.

```ts
import { AbortError, runRetryPolicy } from "@zap-studio/retry";

const controller = new AbortController();

try {
  await runRetryPolicy(
    policy,
    async () => {
      return await doWork();
    },
    { signal: controller.signal },
  );
} catch (error) {
  if (error instanceof AbortError) {
    console.error("Retry aborted:", error.message);
  } else {
    throw error;
  }
}
```

## Non-Throw Mode

With `throwOnExhausted: false`, an abort resolves to `{ ok: false }` with the `AbortError` on `result.error` instead of throwing.

```ts
const controller = new AbortController();

const result = await runRetryPolicy(
  policy,
  async () => {
    return await doWork();
  },
  {
    signal: controller.signal,
    throwOnExhausted: false,
  },
);

if (result.ok) {
  console.log(result.value);
} else {
  console.error("Retry stopped:", result.error);
}
```

## Cancellation Timing

The runner checks the signal:

- before each attempt starts
- after a failed attempt, before consulting the policy
- while waiting between retries (the delay is raced against the signal)

This keeps cancellation responsive without custom orchestration code.

:::warning

The runner does not cancel an `execute` call that is already in flight. Forward the same signal to the underlying operation (for example, `fetch(url, { signal })`) if the work itself should be interruptible.

:::

## Abort Reasons

The abort `reason` you pass to `controller.abort(...)` is normalized into the `AbortError`:

- an `Error` reason becomes the `AbortError` message, with the original error on `cause`
- a non-empty string reason becomes the message
- no reason produces the message `"Retry aborted."`

See [Structured Errors](/retry/errors) for the full `AbortError` reference.
