receta

Async

35 exports from @ontologyio/receta/async.

Everything exported from @ontologyio/receta/async. New here? Start with the Async guide.

Functions

batch

Processes items in batches with optional delay between batches.

Returns a Result to handle errors explicitly. If any batch processing fails, the entire operation returns an Err with details about which batch failed.

fn runs once per batch and receives the whole batch, so the result holds one entry per batch rather than one per item.

Useful for bulk operations like data imports, exports, or processing large datasets without overwhelming the system.

Signature
function batch<T, U>(items: readonly T[], fn: (batch: readonly T[]) => Promise<U>, options?: BatchOptions): Promise<Result<U[], BatchError>>

Fails with BatchError.

Example
import * as R from 'remeda'
import { unwrapOr, tapErr } from '@ontologyio/receta/result'

// Process users in batches of 50
const result = await batch(
  users,
  async (userBatch) => {
    return db.users.insertMany(userBatch)
  },
  {
    batchSize: 50,
    delayBetweenBatches: 1000, // 1 second between batches
  }
)

// Handle with Result pattern
const logged = tapErr(result, error => console.error('Batch failed:', error))
const inserted = unwrapOr(logged, [])

// Send emails in batches to avoid rate limits
const emailResult = await batch(
  emailAddresses,
  async (batch) => sendBulkEmail(batch),
  {
    batchSize: 100,
    delayBetweenBatches: 5000, // 5 seconds between batches
  }
)

// Import large dataset with error handling
const imported = await batch(
  records,
  async (recordBatch) => {
    console.log(`Importing batch of ${recordBatch.length}`)
    return importRecords(recordBatch)
  },
  { batchSize: 1000, concurrency: 2 } // Process 2 batches concurrently
)

See also mapAsync, parallel, batchOrThrow.

batchOrThrow

Throwing variant of batch for backward compatibility.

Use this when you want exceptions instead of Result pattern. Prefer the Result-returning batch for better error handling.

Signature
function batchOrThrow<T, U>(items: readonly T[], fn: (batch: readonly T[]) => Promise<U>, options?: BatchOptions | undefined): Promise<U[]>
Example
// Throws on error
try {
  const results = await batchOrThrow(
    users,
    async (userBatch) => db.users.insertMany(userBatch),
    { batchSize: 50 }
  )
} catch (error) {
  console.error('Batch failed:', error)
}

See also batch, orThrow.

chunk

Split an array into groups the length of size. If array can't be split evenly, the final chunk will be the remaining elements.

Data-first
function chunk<T extends IterableContainer, N extends number>(array: T, size: N): Chunk<T, N>
Data-last
function chunk<N extends number>(size: N): <T extends IterableContainer>(array: T) => Chunk<T, N>
Example
chunk(['a', 'b', 'c', 'd'], 2) // => [['a', 'b'], ['c', 'd']]
   chunk(['a', 'b', 'c', 'd'], 3) // => [['a', 'b', 'c'], ['d']]
Example
chunk(2)(['a', 'b', 'c', 'd']) // => [['a', 'b'], ['c', 'd']]
   chunk(3)(['a', 'b', 'c', 'd']) // => [['a', 'b', 'c'], ['d']]

debounce

Debounces an async function.

Creates a debounced version of an async function that delays invoking until after the specified delay has elapsed since the last call.

Useful for rate-limiting API calls, search inputs, or any operation that shouldn't run on every invocation.

Every call settles. A call that a newer one replaces gets the result of the run that replaced it, and with trailing: false, a call ignored during the delay gets the result of the last leading run. If leading and trailing are both false, nothing can run, so every call rejects. There is no cancel or flush.

Signature
function debounce<TArgs extends readonly unknown[], TReturn>(fn: (...args: TArgs) => Promise<TReturn>, options: DebounceOptions): (...args: TArgs) => Promise<TReturn>
Example
// Debounced search
const searchAPI = debounce(
  async (query: string) => {
    const res = await fetch(`/api/search?q=${query}`)
    return res.json()
  },
  { delay: 300 }
)

// User types "hello" quickly
searchAPI('h')      // Replaced, settles with the result for 'hello'
searchAPI('he')     // Replaced
searchAPI('hel')    // Replaced
searchAPI('hell')   // Replaced
searchAPI('hello')  // Executes after 300ms

// Leading edge debounce (execute immediately, ignore subsequent calls)
const saveData = debounce(
  async (data) => api.save(data),
  { delay: 1000, leading: true, trailing: false }
)

See also throttle.

extractFulfilled

Extracts all fulfilled values from settled results.

Filters out rejected promises and returns only successful values. Useful when you want to process partial results even if some promises failed.

Signature
function extractFulfilled<T>(results: readonly PromiseSettledResult<T>[]): T[]
Example
const results = await promiseAllSettled([
  Promise.resolve(1),
  Promise.reject('error'),
  Promise.resolve(3),
])

const values = extractFulfilled(results)
// => [1, 3]
Example
// Process partial API results
const results = await promiseAllSettled(
  userIds.map((id) => fetchUser(id))
)

const successfulUsers = extractFulfilled(results)
console.log(`Fetched ${successfulUsers.length} of ${userIds.length} users`)

extractRejected

Extracts all rejection reasons from settled results.

Filters out fulfilled promises and returns only error reasons. Useful for logging or analyzing failures.

Signature
function extractRejected<T = unknown>(results: readonly PromiseSettledResult<any>[]): T[]
Example
const results = await promiseAllSettled([
  Promise.resolve(1),
  Promise.reject(new Error('Network error')),
  Promise.reject(new Error('Timeout')),
])

const errors = extractRejected<Error>(results)
// => [Error('Network error'), Error('Timeout')]

errors.forEach((err) => console.error('Failed:', err.message))
Example
// Track failed operations
const results = await promiseAllSettled(
  userIds.map((id) => deleteUser(id))
)

const failures = extractRejected(results)
if (failures.length > 0) {
  console.warn(`${failures.length} deletions failed:`, failures)
}

filterAsync

Filters an array using an async predicate function.

Returns a Result to handle errors explicitly. If any predicate evaluation fails, the entire operation returns an Err with details about the failure.

Data-first
function filterAsync<T>(items: readonly T[], predicate: (item: T, index: number) => Promise<boolean>, options?: ConcurrencyOptions | undefined): Promise<Result<T[], FilterAsyncError>>
Data-last
function filterAsync<T>(predicate: (item: T, index: number) => Promise<boolean>, options?: ConcurrencyOptions | undefined): (items: readonly T[]) => Promise<Result<T[], FilterAsyncError>>

Fails with FilterAsyncError.

Example
import * as R from 'remeda'
import { unwrapOr, tapErr } from '@ontologyio/receta/result'

// Data-first: Filter users who exist
const result = await filterAsync(
  userIds,
  async (id) => {
    const res = await fetch(`/api/users/${id}`)
    return res.ok
  },
  { concurrency: 5 }
)

// Handle with Result pattern
const logged = tapErr(result, error => console.error('Filter failed:', error))
const existingUsers = unwrapOr(logged, [])

// Data-last (pipeAsync awaits each step)
const filtered = await pipeAsync(
  urls,
  filterAsync(isReachable, { concurrency: 3 }),
  (result) => unwrapOr(result, [])
)

See also mapAsync, filterAsyncOrThrow.

filterAsyncOrThrow

Throwing variant of filterAsync for backward compatibility.

Use this when you want exceptions instead of Result pattern. Prefer the Result-returning filterAsync for better error handling.

Data-first
function filterAsyncOrThrow<T>(items: readonly T[], predicate: (item: T, index: number) => Promise<boolean>, options?: ConcurrencyOptions | undefined): Promise<T[]>
Data-last
function filterAsyncOrThrow<T>(predicate: (item: T, index: number) => Promise<boolean>, options?: ConcurrencyOptions | undefined): (items: readonly T[]) => Promise<T[]>
Example
// Throws on error
try {
  const existingUsers = await filterAsyncOrThrow(
    userIds,
    async (id) => {
      const res = await fetch(`/api/users/${id}`)
      return res.ok
    },
    { concurrency: 5 }
  )
} catch (error) {
  console.error('Filter failed:', error)
}

See also filterAsync, orThrow.

mapAsync

Maps over an array with an async function, with optional concurrency control.

Returns a Result to handle errors explicitly. If any mapper function fails, the entire operation returns an Err with details about the failure.

This is more powerful than Promise.all(items.map(fn)) because it:

  • Allows limiting concurrent operations (rate limiting, resource management)
  • Returns Result for explicit error handling
  • Provides detailed error information (which item failed, when concurrency is below the item count)
Data-first
function mapAsync<T, U>(items: readonly T[], fn: (item: T, index: number) => Promise<U>, options?: ConcurrencyOptions | undefined): Promise<Result<U[], MapAsyncError>>
Data-last
function mapAsync<T, U>(fn: (item: T, index: number) => Promise<U>, options?: ConcurrencyOptions | undefined): (items: readonly T[]) => Promise<Result<U[], MapAsyncError>>

Fails with MapAsyncError.

Example
import * as R from 'remeda'
import { unwrapOr, tapErr } from '@ontologyio/receta/result'

// Data-first: Fetch users with concurrency limit
const result = await mapAsync(
  ['user1', 'user2', 'user3'],
  async (id) => fetch(`/api/users/${id}`).then(r => r.json()),
  { concurrency: 2 }
)

// Handle with Result pattern
const logged = tapErr(result, error => console.error('Failed at index:', error.index))
const users = unwrapOr(logged, [])

// Data-last (pipeAsync awaits each step)
const processed = await pipeAsync(
  userIds,
  mapAsync(fetchUser, { concurrency: 5 }),
  (result) => unwrapOr(result, [])
)

// Unlimited concurrency (same as Promise.all but safer)
const all = await mapAsync(urls, fetchJSON)

See also filterAsync, parallel, mapAsyncOrThrow.

mapAsyncOrThrow

Throwing variant of mapAsync for backward compatibility.

Use this when you want exceptions instead of Result pattern. Prefer the Result-returning mapAsync for better error handling.

This is implemented using the orThrow utility from Result module, which converts any Result-returning function to a throwing variant.

Data-first
function mapAsyncOrThrow<T, U>(items: readonly T[], fn: (item: T, index: number) => Promise<U>, options?: ConcurrencyOptions | undefined): Promise<U[]>
Data-last
function mapAsyncOrThrow<T, U>(fn: (item: T, index: number) => Promise<U>, options?: ConcurrencyOptions | undefined): (items: readonly T[]) => Promise<U[]>
Example
// Throws on error
try {
  const users = await mapAsyncOrThrow(
    userIds,
    async (id) => fetchUser(id),
    { concurrency: 5 }
  )
} catch (error) {
  console.error('Failed:', error)
}

See also mapAsync, orThrow.

parallel

Executes an array of async tasks in parallel with optional concurrency limit.

Similar to Promise.all but with concurrency control, and it returns a Result instead of rejecting. Useful when you have a list of async operations and want to limit how many run simultaneously.

Signature
function parallel<T>(tasks: readonly (() => Promise<T>)[], options?: ConcurrencyOptions | undefined): Promise<Result<T[], MapAsyncError>>

Fails with MapAsyncError.

Example
// Execute all tasks with concurrency limit of 3
const results = await parallel(
  [
    () => fetch('/api/users/1'),
    () => fetch('/api/users/2'),
    () => fetch('/api/users/3'),
    () => fetch('/api/users/4'),
    () => fetch('/api/users/5'),
  ],
  { concurrency: 3 }
)

// Unlimited concurrency (same as Promise.all)
const all = await parallel([
  () => fetchUser(1),
  () => fetchPosts(),
  () => fetchComments(),
])

// Generate tasks dynamically
const userIds = [1, 2, 3, 4, 5]
const fetchTasks = userIds.map(id => () => fetchUser(id))
const users = await parallel(fetchTasks, { concurrency: 2 })

See also sequential, mapAsync.

parallelOrThrow

Throwing variant of parallel for backward compatibility.

Use this when you want exceptions instead of Result pattern. Prefer the Result-returning parallel for better error handling.

This is implemented using the orThrow utility from Result module, which converts any Result-returning function to a throwing variant.

Signature
function parallelOrThrow<T>(tasks: readonly (() => Promise<T>)[], options?: ConcurrencyOptions | undefined): Promise<T[]>
Example
// Equivalent to using orThrow directly
const parallelOrThrow = orThrow(parallel)

See also parallel, orThrow.

pipeAsync

Composes async functions from left to right.

Like Remeda's pipe, but handles async functions by awaiting each step before passing the result to the next function. Useful for chaining async transformations in a readable, sequential manner.

Note: All functions are executed sequentially (not in parallel). For parallel execution, use parallel or mapAsync.

Overload 1
function pipeAsync<A, B>(value: A, fn1: (a: A) => B | Promise<B>): Promise<B>
Overload 2
function pipeAsync<A, B, C>(value: A, fn1: (a: A) => B | Promise<B>, fn2: (b: B) => C | Promise<C>): Promise<C>
Overload 3
function pipeAsync<A, B, C, D>(value: A, fn1: (a: A) => B | Promise<B>, fn2: (b: B) => C | Promise<C>, fn3: (c: C) => D | Promise<D>): Promise<D>
Overload 4
function pipeAsync<A, B, C, D, E>(value: A, fn1: (a: A) => B | Promise<B>, fn2: (b: B) => C | Promise<C>, fn3: (c: C) => D | Promise<D>, fn4: (d: D) => E | Promise<E>): Promise<E>
Overload 5
function pipeAsync<A, B, C, D, E, F>(value: A, fn1: (a: A) => B | Promise<B>, fn2: (b: B) => C | Promise<C>, fn3: (c: C) => D | Promise<D>, fn4: (d: D) => E | Promise<E>, fn5: (e: E) => F | Promise<F>): Promise<F>
Overload 6
function pipeAsync<A, B, C, D, E, F, G>(value: A, fn1: (a: A) => B | Promise<B>, fn2: (b: B) => C | Promise<C>, fn3: (c: C) => D | Promise<D>, fn4: (d: D) => E | Promise<E>, fn5: (e: E) => F | Promise<F>, fn6: (f: F) => G | Promise<G>): Promise<G>
Overload 7
function pipeAsync<A, B, C, D, E, F, G, H>(value: A, fn1: (a: A) => B | Promise<B>, fn2: (b: B) => C | Promise<C>, fn3: (c: C) => D | Promise<D>, fn4: (d: D) => E | Promise<E>, fn5: (e: E) => F | Promise<F>, fn6: (f: F) => G | Promise<G>, fn7: (g: G) => H | Promise<H>): Promise<H>
Overload 8
function pipeAsync<A, B, C, D, E, F, G, H, I>(value: A, fn1: (a: A) => B | Promise<B>, fn2: (b: B) => C | Promise<C>, fn3: (c: C) => D | Promise<D>, fn4: (d: D) => E | Promise<E>, fn5: (e: E) => F | Promise<F>, fn6: (f: F) => G | Promise<G>, fn7: (g: G) => H | Promise<H>, fn8: (h: H) => I | Promise<I>): Promise<I>
Overload 9
function pipeAsync<A, B, C, D, E, F, G, H, I, J>(value: A, fn1: (a: A) => B | Promise<B>, fn2: (b: B) => C | Promise<C>, fn3: (c: C) => D | Promise<D>, fn4: (d: D) => E | Promise<E>, fn5: (e: E) => F | Promise<F>, fn6: (f: F) => G | Promise<G>, fn7: (g: G) => H | Promise<H>, fn8: (h: H) => I | Promise<I>, fn9: (i: I) => J | Promise<J>): Promise<J>
Overload 10
function pipeAsync<A, B, C, D, E, F, G, H, I, J, K>(value: A, fn1: (a: A) => B | Promise<B>, fn2: (b: B) => C | Promise<C>, fn3: (c: C) => D | Promise<D>, fn4: (d: D) => E | Promise<E>, fn5: (e: E) => F | Promise<F>, fn6: (f: F) => G | Promise<G>, fn7: (g: G) => H | Promise<H>, fn8: (h: H) => I | Promise<I>, fn9: (i: I) => J | Promise<J>, fn10: (j: J) => K | Promise<K>): Promise<K>
Example
// Sequential API calls
const result = await pipeAsync(
  userId,
  (id) => fetchUser(id),
  (user) => fetchUserPosts(user.id),
  (posts) => posts.filter(p => p.published),
  (posts) => posts.map(p => p.title)
)
// => ['Post 1', 'Post 2', ...]
Example
// Mix sync and async functions
const processOrder = await pipeAsync(
  orderId,
  (id) => fetchOrder(id),          // async
  (order) => order.items,          // sync
  async (items) => {               // async
    const prices = await Promise.all(items.map(getPriceAsync))
    return prices
  },
  (prices) => prices.reduce((a, b) => a + b, 0)  // sync
)
Example
// Error handling with Result pattern
import { tryCatchAsync, map, unwrapOr } from '@ontologyio/receta/result'

const safeResult = await pipeAsync(
  userId,
  (id) => tryCatchAsync(() => fetchUser(id)),
  (userResult) => map(userResult, u => u.email)
)
const email = unwrapOr(safeResult, 'unknown@example.com')
Example
// Data transformation pipeline
const report = await pipeAsync(
  'raw-data.json',
  (file) => fs.readFile(file, 'utf-8'),
  (content) => JSON.parse(content),
  async (data) => {
    const enriched = await enrichData(data)
    return enriched
  },
  (data) => generateReport(data)
)

See also sequential, https://remedajs.com/docs#pipe.

poll

Polls an async function until it returns a value or reaches max attempts.

Useful for waiting on async operations like job completion, order status, or resource availability. Returns Result for explicit error handling.

null, undefined and false mean "not yet". Anything else, including 0 and '', stops polling and becomes the Ok value. If fn throws, the returned promise rejects instead of resolving to an Err.

Signature
function poll<T>(fn: () => Promise<false | T | null | undefined>, options?: PollOptions): Promise<Result<T, PollError>>

Fails with PollError.

Example
import * as R from 'remeda'
import { unwrapOr, mapErr } from '@ontologyio/receta/result'

// Poll until job is complete
const result = await poll(
  async () => {
    const status = await checkJobStatus(jobId)
    return status.state === 'completed' ? status : null
  },
  {
    interval: 1000, // Check every second
    maxAttempts: 30, // Max 30 attempts (30 seconds)
  }
)

// Handle with Result pattern
const job = R.pipe(
  result,
  mapErr(error => {
    if (error.type === 'max_attempts') {
      return 'Job check timed out'
    }
    return 'Job check failed'
  }),
  unwrapOr(null)
)

// Poll with timeout
const order = await poll(
  async () => fetchOrderStatus(orderId),
  {
    interval: 2000,
    maxAttempts: 30,
    timeout: 60000, // Fail after 1 minute
    onPoll: (attempt) => console.log(`Checking... attempt ${attempt}`)
  }
)

// Poll until condition is met
const result = await poll(
  async () => {
    const data = await fetchData()
    return data.ready ? data : null
  },
  {
    interval: 500,
    maxAttempts: 50,
    shouldContinue: (attempt) => attempt < 20
  }
)

See also retry.

promiseAllSettled

Typed wrapper around Promise.allSettled with better type inference.

Unlike Promise.all (which fails fast on first rejection), this waits for all promises to settle (fulfill or reject) before returning results. Provides helper functions to extract fulfilled/rejected values.

Signature
function promiseAllSettled<T>(promises: readonly Promise<T>[]): Promise<PromiseSettledResult<T>[]>
Example
// Basic usage
const results = await promiseAllSettled([
  fetch('/api/users/1'),
  fetch('/api/users/2'),
  fetch('/api/users/999'), // might fail
])

results.forEach((result) => {
  if (result.status === 'fulfilled') {
    console.log('Success:', result.value)
  } else {
    console.log('Error:', result.reason)
  }
})
Example
// Extract only successful results
const results = await promiseAllSettled([
  fetchUser(1),
  fetchUser(2),
  fetchUser(999), // fails
])

const users = extractFulfilled(results)
// => [User1, User2] (failed one excluded)
Example
// Convert to Result pattern for composability
import * as R from 'remeda'
import { map, unwrapOr } from '@ontologyio/receta/result'

const results = await promiseAllSettled([
  fetchUser(1),
  fetchUser(2),
  fetchUser(3),
])

const resultArray = toResults(results)
const usernames = R.pipe(
  resultArray,
  R.map((r) => map(r, (user) => user.name)),
  R.map((r) => unwrapOr(r, 'Unknown'))
)
// => ['John', 'Jane', 'Unknown']

See also https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled.

retry

Retries an async function with exponential backoff, returning a Result.

Useful for handling transient failures like network errors, rate limits, or temporary service unavailability. Returns Result for explicit error handling.

fn runs again when it throws or rejects. A function that returns an Err has succeeded as far as retry can tell, so throw to ask for another attempt.

Signature
function retry<T>(fn: () => Promise<T>, options?: RetryOptions): Promise<Result<T, RetryError>>

Fails with RetryError.

Example
import * as R from 'remeda'
import { map, tapErr, unwrapOr } from '@ontologyio/receta/result'

// Basic retry with defaults (3 attempts, exponential backoff)
const result = await retry(async () => {
  const res = await fetch('/api/data')
  if (!res.ok) throw new Error(`HTTP ${res.status}`)
  return res.json()
})

// Handle with Result pattern
const data = R.pipe(
  result,
  tapErr(error => console.error('Failed after retries:', error)),
  unwrapOr({ default: 'data' })
)

// Custom retry options
const customResult = await retry(
  async () => fetchData(),
  {
    maxAttempts: 5,
    delay: 500,
    backoff: 2,
    maxDelay: 10000,
    jitter: 250, // add 0-250ms of random delay to avoid thundering herds
    shouldRetry: (error, attempt) => {
      // Only retry on specific errors
      return error instanceof NetworkError && attempt < 3
    },
    onRetry: (error, attempt, delay) => {
      console.log(`Retry ${attempt} after ${delay}ms:`, error)
    }
  }
)

// Compose with other Result operations
const processed = R.pipe(
  await retry(() => fetchUser(id)),
  map(user => user.email),
  unwrapOr('noreply@example.com')
)

See also poll, timeout.

sequential

Executes an array of async tasks sequentially (one at a time).

Returns a Result to handle errors explicitly. If any task fails, execution stops and returns an Err with details about which task failed.

Each task waits for the previous one to complete before starting. Useful when tasks have dependencies or when you need strict ordering.

Signature
function sequential<T>(tasks: readonly (() => Promise<T>)[]): Promise<Result<T[], SequentialError>>

Fails with SequentialError.

Example
import * as R from 'remeda'
import { isErr, tapErr, unwrapOr } from '@ontologyio/receta/result'

// Execute tasks one at a time in order
const result = await sequential([
  () => createUser({ name: 'Alice' }),
  () => createPost({ title: 'Hello' }),
  () => publishPost({ id: 1 }),
])

// Handle with Result pattern
const logged = tapErr(result, error => console.error('Task failed at index:', error.taskIndex))
const results = unwrapOr(logged, [])

// Database migrations that must run in order
const migrationResult = await sequential([
  () => runMigration('001_create_users'),
  () => runMigration('002_add_email_index'),
  () => runMigration('003_create_posts'),
])

if (isErr(migrationResult)) {
  console.error('Migration failed:', migrationResult.error)
  // Can rollback completed migrations using completedTasks count
}

// Process items with side effects in order
const logs = await sequential(
  events.map(event => () => processEvent(event))
)

See also parallel, sequentialOrThrow.

sequentialOrThrow

Throwing variant of sequential for backward compatibility.

Use this when you want exceptions instead of Result pattern. Prefer the Result-returning sequential for better error handling.

Signature
function sequentialOrThrow<T>(tasks: readonly (() => Promise<T>)[]): Promise<T[]>
Example
// Throws on error
try {
  const results = await sequentialOrThrow([
    () => createUser({ name: 'Alice' }),
    () => createPost({ title: 'Hello' }),
    () => publishPost({ id: 1 }),
  ])
} catch (error) {
  console.error('Task failed:', error)
}

See also sequential, orThrow.

sleep

Sleep for a specified duration.

Signature
function sleep(ms: number): Promise<void>
Example
// Wait 1 second
await sleep(1000)

// Use in sequence
console.log('Starting...')
await sleep(2000)
console.log('2 seconds later')

throttle

Throttles an async function.

Creates a throttled version of an async function that only invokes at most once per specified delay period.

Unlike debounce (which delays execution until calls stop), throttle guarantees execution at regular intervals regardless of call frequency.

Calls made during the delay share one promise for the next trailing run, which uses the latest arguments. There is no cancel or flush.

Signature
function throttle<TArgs extends readonly unknown[], TReturn>(fn: (...args: TArgs) => Promise<TReturn>, options: ThrottleOptions): (...args: TArgs) => Promise<TReturn>
Example
// Throttled API call (max once per second)
const trackEvent = throttle(
  async (event: string) => {
    await fetch('/api/analytics', {
      method: 'POST',
      body: JSON.stringify({ event })
    })
  },
  { delay: 1000 }
)

// User scrolls rapidly
trackEvent('scroll') // Executes immediately
trackEvent('scroll') // Waits for the trailing run
trackEvent('scroll') // Shares that run, which uses the latest arguments
// ... 1 second passes, and the trailing run executes

// Throttle with trailing edge only
const saveProgress = throttle(
  async (data) => api.save(data),
  { delay: 5000, leading: false, trailing: true }
)

See also debounce.

timeout

Adds a timeout to a promise, returning a Result.

If the promise doesn't resolve within the specified time, returns an Err with TimeoutError for explicit error handling.

Only the timeout becomes an Err: if the promise rejects first, that rejection is rethrown. The promise keeps running after a timeout, because timeout stops waiting rather than cancelling the work.

Signature
function timeout<T>(promise: Promise<T>, ms: number): Promise<Result<T, TimeoutError>>

Fails with TimeoutError.

Example
import * as R from 'remeda'
import { isErr, map, tapErr, unwrapOr } from '@ontologyio/receta/result'

// Basic timeout
const result = await timeout(
  fetch('/api/data').then(r => r.json()),
  5000 // 5 second timeout
)

// Handle with Result pattern
const data = R.pipe(
  result,
  tapErr(error => console.error('Timeout:', error)),
  unwrapOr({ fallback: 'data' })
)

// Compose with other async operations
const processed = R.pipe(
  await timeout(fetchUser(id), 3000),
  map(user => user.email),
  unwrapOr('noreply@example.com')
)

// Check timeout errors explicitly
const result = await timeout(slowOperation(), 1000)
if (isErr(result) && result.error instanceof TimeoutError) {
  console.log('Operation timed out')
}

See also retry, poll.

toResults

Converts settled results to Result array for composability.

Transforms native PromiseSettledResult into Receta's Result type, enabling composition with Result utilities (map, flatMap, etc.).

Signature
function toResults<T, E = unknown>(results: readonly PromiseSettledResult<T>[]): Result<T, E>[]
Example
import * as R from 'remeda'
import { map, unwrapOr } from '@ontologyio/receta/result'

const results = await promiseAllSettled([
  fetchUser(1),
  fetchUser(999), // fails
])

const userResults = toResults(results)
const names = R.pipe(
  userResults,
  R.map((r) => map(r, (user) => user.name)),
  R.map((r) => unwrapOr(r, 'Unknown'))
)
// => ['John', 'Unknown']
Example
import { partition } from '@ontologyio/receta/result'

const results = await promiseAllSettled([
  fetchUser(1),
  fetchUser(2),
  fetchUser(999),
])

const [successes, failures] = partition(toResults(results))
console.log(`${successes.length} succeeded, ${failures.length} failed`)
Example
// Compose with validation
import { flatMap } from '@ontologyio/receta/result'
import { validate } from '@ontologyio/receta/validation'

const results = await promiseAllSettled(
  urls.map((url) => fetch(url).then(r => r.json()))
)

const validated = R.pipe(
  toResults(results),
  R.map((r) => flatMap(r, (data) => validateSchema(userSchema, data)))
)

Values

TimeoutError

Error type for timeout failures.

Signature
const TimeoutError: typeof TimeoutError

Types

BatchError

Error type for batch processing failures.

Signature
interface BatchError {
  readonly type: 'batch_error'
  readonly batchIndex: number
  readonly itemsInBatch: number
  readonly underlyingError: MapAsyncError | unknown
}

BatchOptions

Options for batch operations.

Signature
interface BatchOptions {
  readonly batchSize?: number

  readonly delayBetweenBatches?: number

  readonly concurrency?: number
}

ConcurrencyOptions

Options for concurrent operations.

Signature
interface ConcurrencyOptions {
  readonly concurrency?: number
}

DebounceOptions

Options for debounce operations.

Signature
interface DebounceOptions {
  readonly delay: number

  readonly leading?: boolean

  readonly trailing?: boolean
}

FilterAsyncError

Error type for filterAsync failures.

Signature
interface FilterAsyncError {
  readonly type: 'filter_async_error'
  readonly underlyingError: MapAsyncError
}

MapAsyncError

Error type for mapAsync failures.

Signature
interface MapAsyncError {
  readonly type: 'map_async_error'
  readonly index: number
  readonly item: unknown
  readonly error: unknown
}

PollError

Error type returned when polling fails.

Signature
type PollError =
  | { readonly type: 'max_attempts'; readonly attempts: number }
  | { readonly type: 'timeout'; readonly elapsed: number }
  | { readonly type: 'stopped'; readonly attempt: number }

PollOptions

Options for polling operations.

Signature
interface PollOptions {
  readonly interval?: number

  readonly maxAttempts?: number

  readonly timeout?: number

  readonly shouldContinue?: (attempt: number) => boolean

  readonly onPoll?: (attempt: number) => void
}

RetryError

Error type returned when all retry attempts fail.

Signature
interface RetryError {
  readonly type: 'max_attempts_exceeded'
  readonly lastError: unknown
  readonly attempts: number
}

RetryOptions

Options for retry operations.

Signature
interface RetryOptions {
  readonly maxAttempts?: number

  readonly delay?: number

  readonly backoff?: number

  readonly maxDelay?: number

  readonly jitter?: number

  readonly shouldRetry?: (error: unknown, attempt: number) => boolean

  readonly onRetry?: (error: unknown, attempt: number, delay: number) => void
}

SequentialError

Error type for sequential task execution failures.

Signature
interface SequentialError {
  readonly type: 'sequential_error'
  readonly taskIndex: number
  readonly completedTasks: number
  readonly error: unknown
}

ThrottleOptions

Options for throttle operations.

Signature
interface ThrottleOptions {
  readonly delay: number

  readonly leading?: boolean

  readonly trailing?: boolean
}

On this page