Best AI models for coding

Models that write working code, explain it clearly and spot bugs in yours.

Ranking

  1. 1

    Clean, idiomatic code and strong at multi-file changes.

    Try Claude Sonnet 4.5
  2. 2
    GPT-5OpenAI

    Excellent at tricky algorithms and debugging with long reasoning.

    Try GPT-5
  3. 3

    The most thorough reviewer for large, subtle changes.

    Try Claude Opus 5.5
  4. 4

    Takes in several large files at once and refactors across them.

    Try Gemini 3.1 Pro

Same prompt,
side by side.

Write a TypeScript function `debounceAsync` that debounces an async function, cancels stale calls with AbortController and always resolves the latest call. Include a short usage example.
Claude Sonnet 4.5

Debounced Async Function with Abort Controller

Here's a TypeScript implementation that debounces async functions and properly cancels stale calls:

typescript
type AsyncFunction<T extends any[], R> = (...args: T) => Promise<R>;

function debounceAsync<T extends any[], R>(
  fn: AsyncFunction<T, R>,
  delayMs: number
): AsyncFunction<T, R> {
  let timeoutId: NodeJS.Timeout | null = null;
  let currentController: AbortController | null = null;

  return async (...args: T): Promise<R> => {
    // Cancel previous timeout
    if (timeoutId) {
      clearTimeout(timeoutId);
    }

    // Abort previous pending call
    if (currentController) {
      currentController.abort();
    }

    // Create new abort controller for this call
    currentController = new AbortController();
    const thisController = currentController;

    return new Promise<R>((resolve, reject) => {
      timeoutId = setTimeout(async () => {
        try {
          // Check if this call was aborted before execution
          if (thisController.signal.aborted) {
            reject(new Error('Aborted'));
            return;
          }

          const result = await fn(...args);
          
          // Only resolve if not aborted during execution
          if (!thisController.signal.aborted) {
            resolve(result);
          }
        } catch (error) {
          if (!thisController.signal.aborted) {
            reject(error);
          }
        }
      }, delayMs);
    });
  };
}

Usage Example

typescript
// Simulated API call
async function searchUsers(query: string): Promise<string[]> {
  await new Promise(resolve => setTimeout(resolve, 500));
  return [`User matching "${query}"`];
}

// Create debounced version
const debouncedSearch = debounceAsync(searchUsers, 300);

// Usage in practice
async function handleSearchInput(query: string) {
  try {
    // Only the last call within 300ms window will complete
    const results = await debouncedSearch(query);
    console.log('Results:', results);
  } catch (error) {
    // Stale calls will be rejected with 'Aborted' error
    console.log('Search cancelled');
  }
}

// Rapid calls - only last one resolves
handleSearchInput('j');     // Cancelled
handleSearchInput('jo');    // Cancelled
handleSearchInput('john');  // Executes after 300ms

Key Features

  • Debouncing: Delays execution until calls stop for delayMs
  • Abort Control: Cancels stale promises using AbortController
  • Type Safety: Full TypeScript generics support
  • Latest Win: Only the most recent call within the window completes

This is ideal for search inputs, autocomplete, or any scenario where you want to wait for user input to stabilize before making expensive async operations.

GPT-5

Overview

  • Debounces an async function.
  • Cancels stale calls via AbortController.
  • All callers resolve with the latest call’s result (coalesced resolution).

Implementation (TypeScript)

ts
export type DebouncedAsync<TArgs extends unknown[], TResult> =
  (...args: TArgs) => Promise<TResult>;

/**
 * Debounce an async function. The wrapped function receives an AbortSignal as its first argument.
 * - Stale scheduled or in-flight calls are aborted.
 * - Every invocation resolves/rejects with the latest executed call's outcome.
 */
export function debounceAsync<TArgs extends unknown[], TResult>(
  fn: (signal: AbortSignal, ...args: TArgs) => Promise<TResult>,
  wait: number
): DebouncedAsync<TArgs, TResult> {
  let timer: ReturnType<typeof setTimeout> | undefined;
  let controller: AbortController | null = null;
  let lastArgs: TArgs | null = null;

  // All outstanding callers waiting for the "latest" result
  let pending: Array<{ resolve: (v: TResult) => void; reject: (e: unknown) => void }> = [];

  return (...args: TArgs): Promise<TResult> => {
    lastArgs = args;

    // Cancel any pending timer and in-flight work
    if (timer) clearTimeout(timer);
    if (controller) controller.abort();

    const promise = new Promise<TResult>((resolve, reject) => {
      pending.push({ resolve, reject });
    });

    controller = new AbortController();

    timer = setTimeout(async () => {
      const localController = controller!;
      const callArgs = lastArgs as TArgs;

      try {
        const result = await fn(localController.signal, ...callArgs);

        // If this call was aborted after completion, ignore its result
        if (localController.signal.aborted) return;

        const waiters = pending;
        pending = [];
        for (const { resolve } of waiters) resolve(result);
      } catch (err) {
        // Swallow abort errors; next (latest) call will settle all waiters
        if (localController.signal.aborted) return;

        const waiters = pending;
        pending = [];
        for (const { reject } of waiters) reject(err);
      }
    }, wait);

    return promise;
  };
}

Usage Example

ts
// Debounced search that aborts stale network requests.
// Note: fetch respects AbortSignal natively.
const search = debounceAsync(async (signal, query: string) => {
  const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`, { signal });
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return res.json() as Promise<{ items: string[] }>;
}, 300);

// Rapid calls: only the latest ("react") request runs to completion.
// All three promises resolve with the same final result.
const p1 = search("r");
const p2 = search("rea");
const p3 = search("react");

const result = await p1;           // same as await p2 and await p3
console.log(result.items);         // latest search results
Claude Opus 5.5

debounceAsync: Debounce with Abort and Latest-Result Resolution

Behavior

  • Debounces calls. Only the last call within wait ms actually runs.
  • Aborts stale work. If a new call arrives while a previous execution is in flight, that execution's AbortSignal fires.
  • Always resolves to the latest call. Every pending caller, including superseded ones, receives the result of the most recent call. No promise is left hanging.

Implementation

ts
type AsyncFn<A extends unknown[], R> = (signal: AbortSignal, ...args: A) => Promise<R>;

interface Waiter<R> {
  resolve: (value: R) => void;
  reject: (reason: unknown) => void;
}

export function debounceAsync<A extends unknown[], R>(fn: AsyncFn<A, R>, wait: number) {
  let timer: ReturnType<typeof setTimeout> | undefined;
  let controller: AbortController | undefined;
  let waiters: Waiter<R>[] = [];
  let latestId = 0;

  const settle = (cb: (w: Waiter<R>) => void) => {
    const batch = waiters;
    waiters = [];
    batch.forEach(cb);
  };

  const debounced = (...args: A): Promise<R> => {
    const id = ++latestId;
    clearTimeout(timer);
    controller?.abort(); // cancel any stale in-flight execution

    return new Promise<R>((resolve, reject) => {
      waiters.push({ resolve, reject });

      timer = setTimeout(async () => {
        const ctrl = new AbortController();
        controller = ctrl;
        try {
          const result = await fn(ctrl.signal, ...args);
          if (id === latestId) settle((w) => w.resolve(result));
        } catch (err) {
          // Ignore errors from superseded runs; the newer call will settle waiters.
          if (id === latestId) settle((w) => w.reject(err));
        } finally {
          if (controller === ctrl) controller = undefined;
        }
      }, wait);
    });
  };

  /** Cancel pending/in-flight work and reject all waiters. */
  debounced.cancel = () => {
    latestId++;
    clearTimeout(timer);
    controller?.abort();
    settle((w) => w.reject(new DOMException("Cancelled", "AbortError")));
  };

  return debounced;
}

Usage Example

ts
const search = debounceAsync(async (signal, query: string) => {
  const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`, { signal });
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return (await res.json()) as string[];
}, 300);

// Rapid typing: only "react" is fetched, and all three promises resolve with its results.
search("r").then((r) => console.log("r →", r));
search("re").then((r) => console.log("re →", r));
search("react").then((r) => console.log("react →", r));

// On component unmount:
// search.cancel();

Notes

  • The signal is the first parameter. Your function must pass signal to cancellable APIs such as fetch. Otherwise, aborting only discards the stale result and does not stop the work.
  • Stale errors are swallowed. An AbortError from a superseded run never reaches callers. The latest call settles everyone.
  • Rejecting instead of sharing results. If you prefer that superseded callers reject rather than receive the latest result, reject the older waiters with an AbortError inside debounced before pushing the new waiter.
Gemini 3.1 Pro

Here is a robust implementation of debounceAsync.

It uses a single shared Promise for rapid sequential calls. This ensures that when the debounce window finally closes and the latest async function completes, all preceding stale calls in that batch resolve with the final, correct data.

The Implementation

typescript
export function debounceAsync<TArgs extends any[], TReturn>(
  fn: (signal: AbortSignal, ...args: TArgs) => Promise<TReturn>,
  wait: number
) {
  let timeoutId: ReturnType<typeof setTimeout>;
  let controller: AbortController | null = null;
  let activePromise: Promise<TReturn> | null = null;
  let resolveActive: ((val: TReturn) => void) | null = null;
  let rejectActive: ((err: any) => void) | null = null;

  return (...args: TArgs): Promise<TReturn> => {
    // 1. Reset the debounce timer
    clearTimeout(timeoutId);

    // 2. Abort any previous in-flight network requests/async tasks
    if (controller) {
      controller.abort();
    }

    controller = new AbortController();
    const signal = controller.signal;

    // 3. Create a shared promise for this batch of calls
    if (!activePromise) {
      activePromise = new Promise<TReturn>((resolve, reject) => {
        resolveActive = resolve;
        rejectActive = reject;
      });
    }

    const currentPromise = activePromise;

    timeoutId = setTimeout(async () => {
      try {
        const result = await fn(signal, ...args);
        // If a new call came in while this was fetching, ignore the result
        if (!signal.aborted) {
          resolveActive?.(result);
          activePromise = null; // Reset for the next batch
        }
      } catch (error) {
        if (!signal.aborted) {
          rejectActive?.(error);
          activePromise = null;
        }
      }
    }, wait);

    return currentPromise;
  };
}

Usage Example

This is especially useful for typeahead search inputs. Stale network requests are aborted via the signal, saving bandwidth, and all keystrokes gracefully resolve to the final search result.

typescript
// 1. Define your async function, accepting the AbortSignal
async function searchUsers(signal: AbortSignal, query: string) {
  const response = await fetch(`/api/users?q=${query}`, { signal });
  if (!response.ok) throw new Error("Network error");
  return response.json();
}

// 2. Create the debounced version
const debouncedSearch = debounceAsync(searchUsers, 300);

// 3. Simulate rapid user typing
async function simulateTyping() {
  // These first two calls will be aborted and share the final Promise
  debouncedSearch("Kwi").then(res => console.log("Call 1:", res));
  debouncedSearch("Kwir").then(res => console.log("Call 2:", res));
  
  // This is the final call that actually executes after 300ms
  const finalResult = await debouncedSearch("Kwirl");
  console.log("Call 3:", finalResult);
  
  // Console output (all three log the same final result):
  // Call 1: [{ name: "Kwirl AI" }]
  // Call 2: [{ name: "Kwirl AI" }]
  // Call 3: [{ name: "Kwirl AI" }]
}

More ways to compare