Skip to main content

Overview

The resilientFetch function is a drop-in replacement for the native fetch API that automatically integrates with abort signals from the resilient execution context. When used inside a function wrapped with withResilience and useAbortSignal: true, it automatically inherits the timeout and cancellation behavior.

Function Signature

function resilientFetch(
  input: RequestInfo | URL,
  init?: RequestInit
): Promise<Response>

Parameters

input
RequestInfo | URL
required
The resource to fetch. Can be:
  • A string URL
  • A URL object
  • A Request object
init
RequestInit
Optional fetch options object. All standard fetch options are supported.

Return Value

response
Promise<Response>
A Promise that resolves to the Response object, exactly like native fetch. The request will be automatically cancelled if the active abort signal is triggered.

Implementation Details

From src/index.ts:197-201, the function:
  1. Checks if a signal is already provided in init.signal
  2. If not, uses the active signal from the current resilient execution context
  3. If no signal is available, falls back to standard fetch behavior
  4. Merges the signal into the init options
export const resilientFetch = (input: RequestInfo | URL, init?: RequestInit) => {
  const signal = init?.signal ?? activeSignal;
  if (!signal) return fetch(input, init);
  return fetch(input, { ...init, signal });
};

Examples

Basic Usage (Drop-in Replacement)

import { resilientFetch } from '@oldwhisper/resilience';

// Works exactly like regular fetch
const response = await resilientFetch('/api/users/123');
const user = await response.json();

Automatic Timeout Cancellation

When used inside a resilient function with timeout, the fetch is automatically cancelled:
import { withResilience, resilientFetch } from '@oldwhisper/resilience';

const fetchUserWithTimeout = withResilience(
  async (userId: string) => {
    // This fetch will be automatically cancelled after 3 seconds
    const response = await resilientFetch(`/api/users/${userId}`);
    return await response.json();
  },
  {
    timeoutMs: 3000,
    useAbortSignal: true // Enable automatic abort signal
  }
);

try {
  const user = await fetchUserWithTimeout('123');
} catch (error) {
  if (error.message === 'TimeoutError') {
    console.log('Request timed out and was cancelled');
  }
}

Multiple Requests with Shared Cancellation

const fetchUserProfile = withResilience(
  async (userId: string) => {
    // All these requests share the same abort signal
    const [user, posts, comments] = await Promise.all([
      resilientFetch(`/api/users/${userId}`).then(r => r.json()),
      resilientFetch(`/api/posts?userId=${userId}`).then(r => r.json()),
      resilientFetch(`/api/comments?userId=${userId}`).then(r => r.json())
    ]);
    
    return { user, posts, comments };
  },
  {
    timeoutMs: 5000,
    useAbortSignal: true
  }
);

// If timeout occurs, all three requests are cancelled automatically

Sequential Requests with Early Cancellation

const processWorkflow = withResilience(
  async (workflowId: string) => {
    // Step 1: Create resource
    const createRes = await resilientFetch('/api/resources', {
      method: 'POST',
      body: JSON.stringify({ workflowId })
    });
    const resource = await createRes.json();
    
    // Step 2: Process resource
    const processRes = await resilientFetch(`/api/resources/${resource.id}/process`, {
      method: 'POST'
    });
    
    // Step 3: Get final status
    const statusRes = await resilientFetch(`/api/resources/${resource.id}/status`);
    return await statusRes.json();
  },
  {
    timeoutMs: 10000,
    retries: 2,
    useAbortSignal: true
  }
);

// If timeout occurs at any step, the current request is cancelled
// and the function can retry from the beginning

With Custom Headers and Options

const apiCall = withResilience(
  async (endpoint: string, data: any) => {
    const response = await resilientFetch(`/api/${endpoint}`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${token}`
      },
      body: JSON.stringify(data),
      // Signal is automatically added, no need to pass it
    });
    
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`);
    }
    
    return await response.json();
  },
  {
    timeoutMs: 5000,
    retries: 3,
    useAbortSignal: true,
    retryOn: (error) => {
      // Retry on network errors and 5xx responses
      return error instanceof TypeError || 
             (error.message.startsWith('HTTP 5'));
    }
  }
);

Manual Abort Signal Override

You can still provide your own abort signal if needed:
const controller = new AbortController();

const fetchWithCustomSignal = withResilience(
  async () => {
    // Uses the provided signal instead of the active one
    return await resilientFetch('/api/data', {
      signal: controller.signal
    });
  },
  {
    timeoutMs: 5000,
    useAbortSignal: true
  }
);

// You can abort manually
setTimeout(() => controller.abort(), 2000);

Polling with Automatic Cancellation

const pollForCompletion = withResilience(
  async (jobId: string) => {
    while (true) {
      const response = await resilientFetch(`/api/jobs/${jobId}`);
      const job = await response.json();
      
      if (job.status === 'completed') {
        return job.result;
      }
      
      if (job.status === 'failed') {
        throw new Error('Job failed');
      }
      
      // Wait before next poll
      await sleep(1000);
    }
  },
  {
    timeoutMs: 60000, // 1 minute timeout
    useAbortSignal: true
  }
);

// Both fetch and sleep are cancelled when timeout occurs

File Upload with Timeout

const uploadFile = withResilience(
  async (file: File) => {
    const formData = new FormData();
    formData.append('file', file);
    
    const response = await resilientFetch('/api/upload', {
      method: 'POST',
      body: formData
      // Automatically cancelled on timeout
    });
    
    return await response.json();
  },
  {
    timeoutMs: 30000, // 30 second timeout for uploads
    retries: 1,
    useAbortSignal: true
  }
);

Error Handling

When a request is aborted (either by timeout or manual cancellation), the fetch API throws a DOMException:
try {
  const response = await resilientFetch('/api/data');
  return await response.json();
} catch (error) {
  if (error.name === 'AbortError') {
    console.log('Request was cancelled');
  } else if (error.message === 'TimeoutError') {
    console.log('Operation timed out');
  } else {
    console.log('Other error:', error);
  }
}

Comparison with Native Fetch

FeatureNative FetchresilientFetch
Basic usage
All fetch options
Manual abort signal
Auto abort signal
Timeout integrationManualAutomatic
Works outside resilient context✅ (fallback)

Use Cases

  • API Clients: Build resilient HTTP clients with automatic timeout
  • Data Fetching: Fetch data with automatic cancellation on timeout
  • File Operations: Upload/download with timeout protection
  • Polling: Poll endpoints with automatic timeout
  • GraphQL: Query GraphQL APIs with resilience
  • Webhooks: Call webhooks with timeout and retry

Integration with Active Signal Context

The resilientFetch function leverages the same active signal mechanism as sleep:
  1. When withResilience enables useAbortSignal, it creates an AbortController
  2. The signal is set as the active signal for the execution context
  3. resilientFetch automatically uses this active signal
  4. When timeout occurs, all pending fetch requests are cancelled
  5. The active signal is restored after execution
This eliminates the need to manually thread abort signals through your code - they’re automatically available to all resilient utilities.

Build docs developers (and LLMs) love