typescriptadvanced

AbortController for Cancellation

Cancel async operations with AbortController: fetch requests, timers, streams, and custom operations.

typescript
// Cancellable fetch
async function fetchWithTimeout(url: string, timeoutMs: number): Promise<Response> {
  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), timeoutMs);

  try {
    const response = await fetch(url, { signal: controller.signal });
    return response;
  } finally {
    clearTimeout(timeout);
  }
}

// Cancellable delay
function cancellableDelay(ms: number, signal?: AbortSignal): Promise<void> {
  return new Promise((resolve, reject) => {
    if (signal?.aborted) {
      reject(signal.reason);
      return;
    }

    const timer = setTimeout(resolve, ms);

    signal?.addEventListener('abort', () => {
      clearTimeout(timer);
      reject(signal.reason);
    }, { once: true });
  });
}

// Cancellable async iterator
async function* pollEndpoint(
  url: string,
  intervalMs: number,
  signal: AbortSignal
): AsyncGenerator<unknown> {
  while (!signal.aborted) {
    try {
      const res = await fetch(url, { signal });
      yield await res.json();
      await cancellableDelay(intervalMs, signal);
    } catch (e) {
      if (signal.aborted) return;
      throw e;
    }
  }
}

// Linked controllers (child cancellation)
function linkedController(parent: AbortSignal): AbortController {
  const child = new AbortController();

  if (parent.aborted) {
    child.abort(parent.reason);
  } else {
    parent.addEventListener('abort', () => {
      child.abort(parent.reason);
    }, { once: true });
  }

  return child;
}

// Race multiple operations with cancellation
async function raceWithCancel<T>(
  operations: ((signal: AbortSignal) => Promise<T>)[]
): Promise<T> {
  const controller = new AbortController();

  try {
    return await Promise.race(
      operations.map((op) => op(controller.signal))
    );
  } finally {
    controller.abort(); // cancel losers
  }
}

// Custom cancellable operation
class CancellableTask<T> {
  private controller = new AbortController();
  private promise: Promise<T>;

  constructor(executor: (signal: AbortSignal) => Promise<T>) {
    this.promise = executor(this.controller.signal);
  }

  cancel(reason?: string) {
    this.controller.abort(reason ?? 'Cancelled');
  }

  get result(): Promise<T> {
    return this.promise;
  }
}

// Usage
try {
  const response = await fetchWithTimeout('https://httpbin.org/delay/1', 5000);
  console.log('Status:', response.status);
} catch (e) {
  if ((e as Error).name === 'AbortError') {
    console.log('Request timed out');
  }
}

// Cancellable task
const task = new CancellableTask(async (signal) => {
  await cancellableDelay(1000, signal);
  return 'done';
});

setTimeout(() => task.cancel('Too slow'), 500);
try {
  await task.result;
} catch (e) {
  console.log('Task cancelled:', (e as Error).message);
}

Use Cases

  • Request timeout handling
  • Graceful operation cancellation
  • Race conditions with cleanup

Tags

Related Snippets

Similar patterns you can reuse in the same workflow.