# Timeout



The `timeout()` utility wraps async operations with a time limit, returning `Err<TimeoutError>` if the deadline is exceeded.

Why Timeout? [#why-timeout]

Async operations can hang forever: network partitions, deadlocks, broken external services. Without timeout, a single stalled operation can block your entire application.

Timeout provides:

* **Resource protection** -- Prevent resource leaks from hanging connections
* **Graceful degradation** -- Return a typed error instead of hanging indefinitely
* **Composability** -- Works with `Result.gen` and all Result operations
* **Cancellation integration** -- Respects `AbortSignal` for coordinated cancellation

Timeout with Static Method [#timeout-with-static-method]

You wrap an async operation with a deadline using the static `Result.timeout()` method. This method accepts your async operation and a configuration object that specifies the time limit and error handling behavior. The operation runs until either it completes successfully or the deadline is reached, at which point the timeout error is returned.

Here is a complete example showing how to define a timeout error and wrap a user fetch operation with a 5-second deadline:

```typescript title="timeout-basic.ts"
import { Result, error } from '@deessejs/fp';
import { z } from 'zod';

const TimeoutError = error({
  name: 'TimeoutError',
  schema: z.object({
    deadline: z.number(),
    elapsed: z.number(),
    operation: z.string()
  })
});

// Basic usage - reject after 5000ms
const result = await Result.timeout(
  fetchUser(id),
  { deadline: 5000, timeout: TimeoutError }
);
```

TimeoutOptions [#timeoutoptions]

The timeout configuration is defined by the `TimeoutOptions` interface. Each parameter controls a different aspect of the timeout behavior, from the time boundary to error handling and cancellation semantics.

This interface defines all available configuration options for the timeout function:

```typescript title="timeout-options.ts"
interface TimeoutOptions<T, E = Error> {
  /** Maximum time in milliseconds (> 0) */
  deadline: number;

  /** Constructor for TimeoutError */
  timeout?: ErrorConstructor | ((info: TimeoutInfo) => TaggedError);

  /** External abort signal to respect */
  abortSignal?: AbortSignal;

  /** Fallback on timeout (returns AsyncResult) */
  onTimeout?: () => AsyncResult<T, E>;

  /** What to do with underlying op after timeout: 'abort' (default) or 'continue' */
  resume?: 'abort' | 'continue';

  /** Name for error messages */
  operationName?: string;
}

interface TimeoutInfo {
  deadline: number;
  elapsed: number;
  operation: string;
}
```

**Parameter details:**

* `deadline` (required): The maximum time in milliseconds that the operation is allowed to run. Must be a positive number. When the deadline is exceeded, the timeout error is returned.

* `timeout` (optional): The error constructor or factory function that creates the timeout error. If not provided, a default timeout error is used. You can pass either a standard Error constructor or a tagged error created with the `error()` function.

* `abortSignal` (optional): An external `AbortSignal` that the timeout should respect. If the signal is aborted before the operation completes, an `AbortError` is returned instead of a `TimeoutError`.

* `onTimeout` (optional): A fallback function that returns an alternative result when the timeout occurs. This allows you to provide cached data, default values, or alternative operations without receiving an error.

* `resume` (optional): Controls what happens to the underlying operation after a timeout. When set to `'abort'` (the default), an internal AbortSignal is used to signal the operation to stop. When set to `'continue'`, the operation continues running in the background and you are responsible for tracking and cleaning it up.

* `operationName` (optional): A human-readable name for the operation that appears in error messages, making it easier to identify which operation timed out when debugging.

Behavior [#behavior]

Understanding how timeout behaves in different scenarios helps you design robust systems.

Deadline Boundary Values [#deadline-boundary-values]

The deadline parameter accepts special values that change the behavior:

| Value                   | Behavior                                                  |
| ----------------------- | --------------------------------------------------------- |
| `deadline <= 0`         | Returns `Err<TimeoutError>` immediately with `elapsed: 0` |
| `deadline === Infinity` | No timeout applied, behaves like plain operation          |
| `deadline > 0`          | Normal timeout behavior                                   |

Success Path [#success-path]

When the operation completes before the deadline is reached, the original Result from the operation is returned unchanged. This means if the operation succeeds, you get `Ok<T>`, and if it fails, you get `Err<E>` with whatever error the operation produced.

Timeout Path [#timeout-path]

When the deadline is exceeded, the timeout returns `Err<TimeoutError>` immediately. The behavior of the underlying operation depends on the `resume` option:

* If `resume: 'abort'` (the default), an internal AbortSignal is used to abandon the operation. The operation may or may not actually stop, depending on whether it supports AbortSignal.

* If `resume: 'continue'`, the underlying operation keeps running. You must track and clean up these orphaned operations yourself.

Abort Path [#abort-path]

When the external abort signal is triggered before the operation completes, the timeout returns `Err<AbortError>`. If the signal is already aborted when the timeout starts, the error is returned immediately.

Timeout with Fallback [#timeout-with-fallback]

Sometimes you want to provide an alternative result when a timeout occurs instead of returning an error. You can achieve this by passing an `onTimeout` callback that returns an alternative Result.

This pattern is useful when you have cached data available or want to return a default value rather than propagating the timeout error. The fallback function is only called when the deadline is exceeded, not when the operation succeeds or fails normally.

In this example, if the heavy query times out after 5 seconds, the cached data is returned instead of a TimeoutError:

```typescript title="timeout-fallback.ts"
const result = await Result.timeout(
  fetchHeavyQuery(),
  {
    deadline: 5000,
    timeout: TimeoutError,
    onTimeout: () => Result.ok(cachedData)
  }
);
// Returns cached data instead of TimeoutError
```

External AbortSignal [#external-abortsignal]

You can combine timeout with an external `AbortController` to enable coordinated cancellation from anywhere in your application. This is particularly useful when you need to cancel operations due to user navigation, component unmounting, or other application-level events.

When you pass an `AbortSignal` to the timeout, the timeout monitors both the signal and the deadline. If the signal is aborted first, you receive an `AbortError`. If the deadline is reached first (and the signal is not aborted), you receive a `TimeoutError`.

This example sets up an abort controller that cancels the operation after 3 seconds, while the timeout deadline is set to 5 seconds. If the abort happens first, you get an AbortError:

```typescript title="timeout-abort-signal.ts"
const controller = new AbortController();

// External cancellation
setTimeout(() => controller.abort(), 3000);

const result = await Result.timeout(
  fetchUser(id),
  {
    deadline: 5000,
    timeout: TimeoutError,
    abortSignal: controller.signal
  }
);
// Err<AbortError> if aborted before completion
// Err<TimeoutError> if deadline exceeded before abort
```

Resource Cleanup [#resource-cleanup]

Different applications have different requirements for what happens to operations that time out. The `resume` option gives you control over this behavior.

Resume Abort (default) [#resume-abort-default]

When you set `resume: 'abort'`, the timeout uses an internal AbortSignal to signal the operation to stop. This is the default behavior because it allows resources to be reclaimed quickly. However, there is no guarantee that the operation will actually stop, since not all operations respect AbortSignal. The memory and resources of the abandoned operation are eventually reclaimed when it settles.

Resume Continue [#resume-continue]

When you set `resume: 'continue'`, the underlying operation continues running after the timeout returns. You are responsible for tracking these orphaned operations and cleaning them up. Use this option when your operation does not support AbortSignal but must complete for correctness, such as when writing to a database or completing a file transfer.

Error Types [#error-types]

The timeout function returns different error types depending on what went wrong.

TimeoutError [#timeouterror]

The TimeoutError is returned when the deadline is exceeded. It contains information about the timeout threshold, how much time actually elapsed, and the name of the operation that timed out. You define this error using the `error()` function from the library.

This is the schema definition for the TimeoutError:

```typescript title="timeout-error-definition.ts"
const TimeoutError = error({
  name: 'TimeoutError',
  schema: z.object({
    deadline: z.number(),
    elapsed: z.number(),
    operation: z.string()
  })
});
```

| Property    | Type             | Description                      |
| ----------- | ---------------- | -------------------------------- |
| `_tag`      | `"TimeoutError"` | Discriminator                    |
| `deadline`  | `number`         | The timeout threshold in ms      |
| `elapsed`   | `number`         | Actual time before timeout in ms |
| `operation` | `string`         | Name of the timed-out operation  |

AbortError [#aborterror]

The AbortError is returned when the external abort signal is triggered before the operation completes. The reason property optionally contains whatever value was passed to the abort call.

This is the schema definition for the AbortError:

```typescript title="abort-error-definition.ts"
const AbortError = error({
  name: 'AbortError',
  schema: z.object({
    reason: z.unknown().optional()
  })
});
```

| Property | Type           | Description                  |
| -------- | -------------- | ---------------------------- |
| `_tag`   | `"AbortError"` | Discriminator                |
| `reason` | `unknown`      | The abort reason if provided |

Using with Retry [#using-with-retry]

Timeout integrates well with the retry system. When you configure retry with a `maxTotalTime` option, it uses the timeout internally to enforce a global deadline across all retry attempts. This ensures that no matter how many retries occur, the entire retry operation completes within your specified time.

You can also use timeout with retry to distinguish between transient errors and timeout errors, so you can decide whether to retry based on the type of failure:

```typescript title="timeout-retry.ts"
// Retry with deadline (retry stops when deadline reached)
const result = await Result.retry(
  fetchUser(id),
  {
    attempts: 3,
    maxTotalTime: 10000,  // Global timeout - uses timeout internally
    shouldRetry: (err) => err._tag !== 'TimeoutError'
  }
);
```

Complete Example [#complete-example]

This example demonstrates a real-world pattern: fetching data from a network endpoint with timeout protection, handling network errors, and falling back to cached data on timeout.

You define custom errors for network and timeout scenarios, then create a function that wraps the fetch operation with timeout. The function attempts to fetch and parse JSON, returns a network error if that fails, and applies a timeout deadline to prevent hanging. When a timeout occurs, an optional fallback returns cached data.

```typescript title="complete-example.ts"
import { Result, error } from '@deessejs/fp';
import { z } from 'zod';

const NetworkError = error({
  name: 'NetworkError',
  schema: z.object({ url: z.string(), cause: z.unknown().optional() })
});

const TimeoutError = error({
  name: 'TimeoutError',
  schema: z.object({
    deadline: z.number(),
    elapsed: z.number(),
    operation: z.string()
  })
});

// Fetch with timeout
async function fetchWithTimeout(url: string, deadline: number) {
  return Result.timeout(
    Result.attemptAsync(
      () => fetch(url).then(r => r.json()),
      (cause) => NetworkError({ url, cause })
    ),
    {
      deadline,
      timeout: TimeoutError,
      operationName: `fetch(${url})`
    }
  );
}

// With fallback
const result = await Result.timeout(
  fetchHeavyQuery(),
  {
    deadline: 2000,
    timeout: TimeoutError,
    onTimeout: () => Result.ok(getCachedData())
  }
);
```

See Also [#see-also]

<Cards>
  <Card title="Sleep" href="./sleep">
    Sleep utilities
  </Card>

  <Card title="Retry" href="./retry">
    Retry with backoff
  </Card>

  <Card title="Repeat" href="./repeat">
    Repeated operation execution
  </Card>
</Cards>
