Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/visible/cruel/llms.txt

Use this file to discover all available pages before exploring further.

Overview

The Timeout pattern ensures that operations complete within a specified time limit. If an operation exceeds the timeout, it’s canceled and a CruelTimeoutError is thrown. This prevents resources from being held indefinitely by slow or hanging operations.

When to Use

  • External API calls that might hang
  • Database queries that could run too long
  • Any I/O operation without built-in timeouts
  • Preventing resource exhaustion from slow operations
  • Enforcing SLA requirements

API Reference

Function Signature

function withTimeout<T extends AnyFn>(fn: T, options: TimeoutOptions): T

Options

ms
number
required
Timeout duration in milliseconds. Must be at least 1. If the operation doesn’t complete within this time, it’s canceled.
onTimeout
() => void
Callback function executed when the timeout is triggered.

Examples

Basic Timeout

import { withTimeout } from 'cruel'

const fetchUser = async (id: string) => {
  const response = await fetch(`https://api.example.com/users/${id}`)
  return response.json()
}

const timedFetch = withTimeout(fetchUser, {
  ms: 5000,  // 5 second timeout
})

try {
  const user = await timedFetch('123')
} catch (error) {
  if (error instanceof CruelTimeoutError) {
    console.log('Request timed out')
  }
}

With Timeout Callback

import { withTimeout, CruelTimeoutError } from 'cruel'

const monitoredAPI = withTimeout(apiCall, {
  ms: 3000,
  onTimeout: () => {
    console.warn('API call timed out after 3s')
    metrics.increment('api.timeout')
  },
})

Database Query Timeout

const executeQuery = async (sql: string) => {
  const connection = await pool.getConnection()
  try {
    return await connection.query(sql)
  } finally {
    connection.release()
  }
}

const timedQuery = withTimeout(executeQuery, {
  ms: 10000,  // 10 second timeout for queries
  onTimeout: () => {
    console.error('Database query timed out')
  },
})

Multiple Timeout Tiers

// Fast operations
const fastAPI = withTimeout(getCachedData, { ms: 500 })

// Standard operations
const standardAPI = withTimeout(fetchData, { ms: 5000 })

// Slow operations
const slowAPI = withTimeout(generateReport, { ms: 30000 })

Per-Environment Timeouts

const timeouts = {
  development: 30000,  // Longer timeouts for debugging
  staging: 10000,
  production: 5000,
}

const apiCall = withTimeout(fetchData, {
  ms: timeouts[process.env.NODE_ENV] || 5000,
})

Race with Timeout

// Manually racing with a timeout
async function fetchWithTimeout<T>(
  promise: Promise<T>,
  ms: number
): Promise<T> {
  const timeoutPromise = new Promise<never>((_, reject) => {
    setTimeout(() => reject(new CruelTimeoutError()), ms)
  })
  
  return Promise.race([promise, timeoutPromise])
}

// Better: use withTimeout
const timedFetch = withTimeout(fetch, { ms: 5000 })

Combining with Other Patterns

Timeout + Retry

import { withTimeout, withRetry } from 'cruel'

// Apply timeout first, then retry
const resilientAPI = withRetry(
  withTimeout(apiCall, { ms: 5000 }),
  {
    attempts: 3,
    delay: 1000,
    backoff: 'exponential',
  }
)

// Each retry attempt gets 5 seconds

Timeout + Fallback

import { withTimeout, withFallback } from 'cruel'

const safeAPI = withFallback(
  withTimeout(fetchConfig, { ms: 3000 }),
  {
    fallback: defaultConfig,
    onFallback: (error) => {
      if (error instanceof CruelTimeoutError) {
        console.log('Config fetch timed out, using defaults')
      }
    },
  }
)

Timeout + Circuit Breaker

import { withTimeout, createCircuitBreaker } from 'cruel'

// Timeouts count as failures for circuit breaker
const resilientAPI = createCircuitBreaker(
  withTimeout(apiCall, { ms: 5000 }),
  {
    threshold: 5,
    timeout: 30000,
  }
)

Timeout + Bulkhead

import { withTimeout, createBulkhead } from 'cruel'

// Prevent slow operations from holding bulkhead slots
const resilientAPI = createBulkhead(
  withTimeout(apiCall, { ms: 5000 }),
  {
    maxConcurrent: 10,
    maxQueue: 50,
  }
)

With Compose

import { cruel } from 'cruel'

const resilientAPI = cruel.compose(apiCall, {
  timeoutMs: 5000,  // 5 second timeout
  retry: {
    attempts: 3,
    backoff: 'exponential',
  },
  fallback: defaultValue,
})

Advanced Examples

Dynamic Timeout Based on Operation

function createAdaptiveTimeout<T extends AnyFn>(fn: T) {
  return async (...args: Parameters<T>): Promise<ReturnType<T>> => {
    const operationType = args[0]?.type
    const timeoutMs = {
      'quick': 1000,
      'standard': 5000,
      'batch': 30000,
    }[operationType] || 5000

    return withTimeout(fn, { ms: timeoutMs })(...args)
  }
}

Timeout with Progress Tracking

function withProgressTimeout<T extends AnyFn>(
  fn: T,
  timeoutMs: number,
  onProgress: (elapsed: number, total: number) => void
) {
  return async (...args: Parameters<T>): Promise<ReturnType<T>> => {
    const startTime = Date.now()
    const progressInterval = setInterval(() => {
      const elapsed = Date.now() - startTime
      onProgress(elapsed, timeoutMs)
    }, 1000)

    try {
      return await withTimeout(fn, { ms: timeoutMs })(...args)
    } finally {
      clearInterval(progressInterval)
    }
  }
}

const trackedAPI = withProgressTimeout(
  apiCall,
  10000,
  (elapsed, total) => {
    console.log(`Progress: ${elapsed}ms / ${total}ms`)
  }
)

Cascading Timeouts

// Parent operation has longer timeout than children
const childOperation = withTimeout(fetchData, { ms: 3000 })

const parentOperation = withTimeout(
  async () => {
    const result1 = await childOperation('data1')
    const result2 = await childOperation('data2')
    return combine(result1, result2)
  },
  { ms: 10000 }  // Parent timeout is longer
)

Timeout with Cleanup

function withTimeoutCleanup<T extends AnyFn>(
  fn: T,
  timeoutMs: number,
  cleanup: () => void
) {
  return withTimeout(
    async (...args: Parameters<T>) => {
      try {
        return await fn(...args)
      } catch (error) {
        cleanup()
        throw error
      }
    },
    {
      ms: timeoutMs,
      onTimeout: cleanup,
    }
  )
}

const apiWithCleanup = withTimeoutCleanup(
  uploadFile,
  30000,
  () => {
    console.log('Cleaning up partial upload')
    cleanupTempFiles()
  }
)

Error Handling

Timeout throws CruelTimeoutError:
import { CruelTimeoutError } from 'cruel'

try {
  await timedAPI()
} catch (error) {
  if (error instanceof CruelTimeoutError) {
    console.log('Operation timed out')
    // error.name === 'CruelTimeoutError'
    // error.code === 'CRUEL_TIMEOUT'
    // error.message === 'cruel: injected timeout'
  }
}

Best Practices

  • Set realistic timeouts: Too short and you’ll timeout valid operations, too long and you won’t prevent hangs
  • Timeout external calls: Always timeout network requests and external API calls
  • Layer timeouts: Parent operations should have longer timeouts than children
  • Monitor timeout rates: High timeout rates indicate performance issues
  • Combine with retries: Use retries to recover from transient timeout issues
  • Use appropriate granularity: Database queries might need 10s, cache lookups 100ms
  • Clean up on timeout: Release resources when operations timeout
  • Log timeout events: Track which operations are timing out for debugging

Timeout Guidelines

Operation TypeRecommended TimeoutReasoning
Cache lookup100-500msShould be very fast
Database query5-10sDepends on complexity
External API5-30sNetwork latency + processing
File upload60-300sDepends on file size
Report generation30-120sCPU intensive
Health check1-3sMust be fast
WebSocket message10-30sReal-time expectations

Common Patterns

API Client with Timeouts

class APIClient {
  constructor(
    private baseURL: string,
    private defaultTimeout: number = 5000
  ) {}

  async request(endpoint: string, options?: { timeout?: number }) {
    const timeout = options?.timeout || this.defaultTimeout
    
    const fetchFn = withTimeout(
      async () => {
        const response = await fetch(`${this.baseURL}${endpoint}`)
        return response.json()
      },
      { ms: timeout }
    )

    return fetchFn()
  }

  async quickRequest(endpoint: string) {
    return this.request(endpoint, { timeout: 1000 })
  }

  async slowRequest(endpoint: string) {
    return this.request(endpoint, { timeout: 30000 })
  }
}

Service with Operation-Specific Timeouts

class UserService {
  async getUser(id: string) {
    // Fast read operation
    return withTimeout(this.fetchUser, { ms: 3000 })(id)
  }

  async createUser(data: User) {
    // Slower write operation
    return withTimeout(this.insertUser, { ms: 10000 })(data)
  }

  async generateUserReport(id: string) {
    // Very slow operation
    return withTimeout(this.buildReport, { ms: 60000 })(id)
  }

  private async fetchUser(id: string) { /* ... */ }
  private async insertUser(data: User) { /* ... */ }
  private async buildReport(id: string) { /* ... */ }
}