Fairy
Resources

How to Avoid: External Call Without Timeout

August 10, 2026 · 9-minute read · Fairy

The short answer

To prevent external calls without timeout in AI-generated code, wrap every fetch or HTTP request with an AbortController and a timeout. Create an AbortController, call setTimeout to trigger controller.abort() after your limit (e.g., 10 seconds), pass the signal to fetch, and handle AbortError in your catch block. This prevents indefinite hangs when third-party services slow down or become unresponsive.

The Direct Answer: Always Set a Timeout with AbortController

Every external HTTP call in production code needs a timeout. The correct pattern uses AbortController to cancel requests that exceed your limit:

async function fetchWithTimeout(url, options = {}, timeoutMs = 10000) {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
  
  try {
    const response = await fetch(url, {
      ...options,
      signal: controller.signal
    });
    return response;
  } catch (error) {
    if (error.name === 'AbortError') {
      throw new Error(`Request to ${url} timed out after ${timeoutMs}ms`);
    }
    throw error;
  } finally {
    clearTimeout(timeoutId);
  }
}

This pattern prevents indefinite hangs when third-party services become slow or unresponsive—a failure mode that AI-generated code consistently misses.

Why AI Produces Code Without Timeouts

AI models generate code that works in ideal conditions. When you ask an LLM to "fetch data from an API," it produces the happy path:

// What AI typically generates
const response = await fetch('https://api.example.com/data');
const data = await response.json();

This code is syntactically correct and works perfectly when the API responds quickly. The problem is that AI models are trained on tutorials, documentation, and example code—materials that prioritize clarity over production hardening.

Timeouts are defensive code. They protect against conditions the primary logic doesn't handle: network partitions, overloaded services, DNS failures that hang rather than reject. AI doesn't anticipate these scenarios because they don't appear in the training distribution. The model sees thousands of examples of fetch(url) returning successfully, and very few examples of fetch(url) hanging for 90 seconds before the connection resets.

This isn't a flaw in any specific model—it's a structural gap in how AI understands production requirements. The code satisfies the stated requirement ("fetch data") without considering operational context ("don't hang indefinitely if the service is slow").

The Real Cost of Missing Timeouts

A hanging HTTP request seems like a minor problem until it cascades. Here's what actually happens in production:

Serverless environments (Lambda, Cloud Functions): Your function waits at the network call until the platform's hard timeout kills it. If your function timeout is 30 seconds and the external call hangs for 28 seconds, you've burned 28 seconds of compute with no useful work. Multiply by concurrent invocations during an outage, and you're paying for infrastructure that's doing nothing.

Long-running servers: Each hanging request holds a connection slot. Node.js HTTP agents default to a limited connection pool per host. When requests to a slow third party accumulate, new requests queue behind them. Eventually, your own endpoints start timing out because they're waiting for connections to a service that isn't responding.

User experience: A page that hangs with a spinner is worse than a page that shows an error after 5 seconds. Users don't know if the operation is still running or if they should retry. Timeouts with proper error handling let you fail fast and communicate clearly.

Cascading failures: Service A calls Service B, which calls Service C. If C hangs and B has no timeout, A waits on B indefinitely. A single slow downstream dependency can propagate delays through your entire system.

Detecting Timeout-less Calls in AI-Generated Code

When reviewing AI-generated code, scan for these patterns:

Raw fetch calls without AbortController

// Missing timeout - will hang indefinitely
const response = await fetch(apiUrl);

Look for fetch calls that don't pass a signal option. Any fetch without signal: controller.signal is suspect.

Axios without timeout configuration

// Missing timeout
const response = await axios.get(apiUrl);

// Also missing - timeout: 0 means no timeout
const response = await axios.get(apiUrl, { timeout: 0 });

Axios accepts a timeout option, but it must be explicitly set. A missing option or a value of 0 means infinite wait.

HTTP client instantiation without defaults

// No default timeout - every call must specify its own
const client = axios.create({
  baseURL: 'https://api.example.com'
});

If you're using an HTTP client instance, timeouts should be set at the instance level so every call inherits them.

Async calls without error handling

This compounds the timeout problem. AI-generated code often uses await without try/catch:

// Double problem: no timeout AND no error handling
const response = await fetch(apiUrl);
const data = await response.json();
processData(data);

If this code is inside an async function without try/catch, both timeout errors (when you add them) and network errors will become unhandled rejections that crash the process or return a 500 with no useful information.

The Complete Timeout Pattern

Here's a production-ready implementation that handles timeouts, retries, and error propagation:

class ExternalCallError extends Error {
  constructor(message, { url, cause, isTimeout = false }) {
    super(message);
    this.name = 'ExternalCallError';
    this.url = url;
    this.cause = cause;
    this.isTimeout = isTimeout;
  }
}

async function fetchWithTimeout(url, options = {}, timeoutMs = 10000) {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
  
  try {
    const response = await fetch(url, {
      ...options,
      signal: controller.signal
    });
    
    if (!response.ok) {
      throw new ExternalCallError(
        `HTTP ${response.status}: ${response.statusText}`,
        { url, isTimeout: false }
      );
    }
    
    return response;
  } catch (error) {
    if (error.name === 'AbortError') {
      throw new ExternalCallError(
        `Request timed out after ${timeoutMs}ms`,
        { url, cause: error, isTimeout: true }
      );
    }
    if (error instanceof ExternalCallError) {
      throw error;
    }
    throw new ExternalCallError(
      `Network error: ${error.message}`,
      { url, cause: error, isTimeout: false }
    );
  } finally {
    clearTimeout(timeoutId);
  }
}

async function fetchWithRetry(url, options = {}, { 
  timeoutMs = 10000, 
  maxRetries = 3,
  backoffMs = 100 
} = {}) {
  let lastError;
  
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      return await fetchWithTimeout(url, options, timeoutMs);
    } catch (error) {
      lastError = error;
      
      // Don't retry on non-timeout client errors
      if (!error.isTimeout && error.message.includes('HTTP 4')) {
        throw error;
      }
      
      if (attempt < maxRetries - 1) {
        await new Promise(r => setTimeout(r, backoffMs * Math.pow(2, attempt)));
      }
    }
  }
  
  throw lastError;
}

Key elements of this pattern:

  1. AbortController with clearTimeout in finally: The timeout is always cleaned up, even if the request succeeds or fails for other reasons.

  2. Custom error class with context: The error includes the URL, whether it was a timeout, and the original cause. This makes debugging and monitoring straightforward.

  3. Retry with exponential backoff: Transient failures get retried, but with increasing delays to avoid hammering a struggling service.

  4. Error preservation: The retry loop captures the last error and rethrows it. Unlike silent swallowing (a common AI bug), this surfaces what actually went wrong.

  5. Smart retry logic: Client errors (4xx) aren't retried since they won't succeed on retry.

Axios Equivalent

If you're using axios, the pattern is simpler because timeout is a first-class option:

const apiClient = axios.create({
  baseURL: 'https://api.example.com',
  timeout: 10000, // 10 seconds
});

// Add response interceptor for consistent error handling
apiClient.interceptors.response.use(
  response => response,
  error => {
    if (error.code === 'ECONNABORTED') {
      throw new ExternalCallError(
        `Request timed out after ${error.config.timeout}ms`,
        { url: error.config.url, cause: error, isTimeout: true }
      );
    }
    throw error;
  }
);

Set the timeout at the client level so you don't need to remember it on every call.

Timeout Values: How to Choose

Your timeout should be shorter than any infrastructure timeout upstream of your code:

ContextRecommended Timeout
User-facing API responding to frontend5-10 seconds
Background job calling payment provider15-30 seconds
Lambda function (30s timeout)20-25 seconds max
Health check endpoints2-3 seconds

Leave buffer between your timeout and the infrastructure limit. If your Lambda times out at 30 seconds, set HTTP timeouts to 20 seconds. This ensures your code can handle the timeout gracefully—log it, return an error response, trigger an alert—rather than being killed mid-execution.

Catching This in Code Review

When reviewing AI-generated code, these checks take 30 seconds and catch the majority of timeout issues:

  1. Search for fetch( — Every occurrence should have a corresponding AbortController or be wrapped in a helper that provides one.

  2. Search for axios without .timeout — If using axios, confirm timeout is set either in create() or on each call.

  3. Check third-party SDK calls — Payment providers, email services, and analytics APIs often use their own HTTP clients. Verify their timeout configuration.

  4. Look for await without try/catch — Timeout errors need handling. Unhandled awaits mean timeouts crash the process.

Fairy's code review for AI-generated code catches these patterns automatically, flagging external calls without timeout alongside related issues like async without error handling and retry helpers that silently swallow errors.

The Compound Problem: Timeouts Without Error Handling

A timeout is only useful if you handle the error it produces. AI-generated code often has both problems:

// AI-generated: no timeout, no error handling
async function getUser(userId) {
  const response = await fetch(`/api/users/${userId}`);
  return response.json();
}

Adding a timeout without adding try/catch just changes how the code fails:

// Better, but still incomplete
async function getUser(userId) {
  const controller = new AbortController();
  setTimeout(() => controller.abort(), 10000);
  const response = await fetch(`/api/users/${userId}`, { 
    signal: controller.signal 
  });
  return response.json();
}

Now you get an AbortError instead of hanging forever, but that error still propagates as an unhandled rejection. The correct fix addresses both:

// Complete solution
async function getUser(userId) {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), 10000);
  
  try {
    const response = await fetch(`/api/users/${userId}`, { 
      signal: controller.signal 
    });
    return response.json();
  } catch (error) {
    if (error.name === 'AbortError') {
      console.error(`User fetch timed out for ${userId}`);
      return null; // or throw a domain-specific error
    }
    console.error(`User fetch failed for ${userId}:`, error);
    throw error;
  } finally {
    clearTimeout(timeoutId);
  }
}

Preventing This at Scale

Individual code review catches these issues, but systematic prevention requires tooling:

  1. Lint rules: ESLint plugins can warn on bare fetch calls without signal options.

  2. Wrapper functions: Provide a team-standard fetchWithTimeout that everyone uses instead of raw fetch. Make bare fetch a lint error.

  3. HTTP client defaults: If using axios, create a configured instance and export only that. Never export the bare axios import.

  4. AI review automation: Tools like Fairy Scout scan pull requests specifically for patterns AI gets wrong, including external calls without timeout.

The goal is making the safe pattern the easy pattern. If the timeout-protected wrapper is what developers import by default, they can't accidentally introduce hanging calls.

Summary

External calls without timeout are one of the most common AI code bugs because they only fail under adverse conditions—exactly the conditions AI doesn't model. The fix is straightforward:

  1. Every fetch needs AbortController with a timeout
  2. Every axios call needs an explicit timeout option
  3. Timeout errors need try/catch handling
  4. Retries need exponential backoff and error preservation

When reviewing AI-generated code, treat any external HTTP call without explicit timeout configuration as a bug that needs fixing before production. The code will work in testing. It will fail when a third party has an incident—which is exactly when you need your code to be resilient.

For systematic detection of this and related patterns in your AI-generated code, explore Fairy's code verification or try Fairy Scout on your next pull request.

Frequently asked questions

Why doesn't fetch have a built-in timeout?

The Fetch API was designed to be low-level and composable. Timeouts were intentionally left to AbortController, which provides a unified cancellation mechanism that works with any async operation, not just HTTP requests.

What happens when an external call hangs without a timeout?

The request waits indefinitely for a response. In serverless environments, this burns compute time until the platform kills the function. In long-running servers, it consumes connection pool slots and can cascade into resource exhaustion and service degradation.

How do I choose the right timeout value?

Base it on your SLA requirements and the external service's expected latency. Most API calls should complete in 5-15 seconds. Payment or critical operations may warrant 30 seconds. Always set a timeout lower than your infrastructure's hard limit (Lambda timeout, load balancer idle timeout).

Does axios handle timeouts automatically?

Axios accepts a timeout option that works out of the box, unlike fetch. However, AI-generated code often omits this option. Always verify that timeout is explicitly set in axios configuration.

Should I retry after a timeout?

Yes, but with exponential backoff and a maximum retry count. Immediate retries during an outage can amplify the problem. Combine timeout handling with a retry strategy that captures and eventually surfaces the final error rather than silently swallowing failures.


Have AI-generated work you’d want verified? Connect with a Fairy → or run a free check with Scout.

More resources