Common Patterns & Recipes
Real-world patterns for async operations using Receta's async module. All examples are production-ready and use actual APIs.
Table of Contents
- Result Integration Patterns
- API Integration Patterns
- Bulk Operations
- Real-time Updates
- Error Recovery
- Performance Optimization
Result Integration Patterns
Async functions return a Result for type-safe error handling. This section shows how to build production-ready patterns on that.
Pattern: Resilient API Client with Result
import * as R from 'remeda'
import { retry, timeout, mapAsync } from '@ontologyio/receta/async'
import { mapErr, partition, tapErr, unwrapOr, isOk, isErr } from '@ontologyio/receta/result'
import type { Result } from '@ontologyio/receta/result'
type APIError = {
type: 'network' | 'timeout' | 'rate_limit' | 'not_found'
message: string
retryable: boolean
}
class ResilientAPIClient {
constructor(private baseURL: string, private apiKey: string) {}
private async request<T>(
endpoint: string,
options?: RequestInit
): Promise<Result<T, APIError>> {
const result = await retry(
async () => {
const res = await timeout(
fetch(`${this.baseURL}${endpoint}`, {
...options,
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json',
...options?.headers,
},
}),
10000 // 10s timeout
)
if (isErr(res)) {
throw new Error('Timeout')
}
const response = res.value
if (!response.ok) {
throw new Error(`HTTP ${response.status}`)
}
return response.json() as Promise<T>
},
{
maxAttempts: 3,
delay: 1000,
backoff: 2,
shouldRetry: (error, attempt) => {
// Only retry network errors, not client errors
return error instanceof Error && !error.message.includes('4')
},
}
)
return R.pipe(
result,
mapErr((error): APIError => {
if (error.lastError instanceof Error) {
if (error.lastError.message.includes('Timeout')) {
return { type: 'timeout', message: 'Request timed out', retryable: true }
}
if (error.lastError.message.includes('429')) {
return { type: 'rate_limit', message: 'Rate limit exceeded', retryable: true }
}
if (error.lastError.message.includes('404')) {
return { type: 'not_found', message: 'Resource not found', retryable: false }
}
}
return { type: 'network', message: 'Network error', retryable: true }
})
)
}
async getUser(id: string) {
return this.request<User>(`/users/${id}`)
}
async getUsers(ids: string[]) {
return mapAsync(
ids,
(id) => this.getUser(id),
{ concurrency: 5 }
)
}
}
// Usage - no try-catch needed!
const client = new ResilientAPIClient('https://api.example.com', 'key')
const userResult = await client.getUser('123')
const user = R.pipe(
userResult,
tapErr(err => console.error('Failed to fetch user:', err)),
unwrapOr({ id: '123', name: 'Unknown' })
)
const usersResult = await client.getUsers(['1', '2', '3'])
// getUser returns a Result, so each user carries its own outcome
if (isOk(usersResult)) {
const [users, failures] = partition(usersResult.value)
console.log(`Loaded ${users.length} users, ${failures.length} failed`)
}Pattern: Batch Processing with Result
import { batch } from '@ontologyio/receta/async'
import { mapErr, partition } from '@ontologyio/receta/result'
import type { Result } from '@ontologyio/receta/result'
async function batchProcessWithResult<T, U>(
items: T[],
processor: (item: T) => Promise<Result<U, string>>,
options: { batchSize: number; delayBetweenBatches: number }
): Promise<{ successes: U[]; failures: Array<{ item: T; error: string }> }> {
const results: Array<Result<U, { item: T; error: string }>> = []
await batch(
items,
async (batchItems) => {
for (const item of batchItems) {
const result = await processor(item)
results.push(mapErr(result, error => ({ item, error })))
}
},
options
)
const [successes, failures] = partition(results)
return { successes, failures }
}
// Usage
const { successes, failures } = await batchProcessWithResult(
records,
async (record) => processRecord(record),
{ batchSize: 100, delayBetweenBatches: 1000 }
)
console.log(`Processed ${successes.length} successfully`)
console.log(`Failed ${failures.length}:`, failures)Pattern: Parallel Operations with All Success
import { parallel, type MapAsyncError } from '@ontologyio/receta/async'
import { isOk, map } from '@ontologyio/receta/result'
import type { Result } from '@ontologyio/receta/result'
// Fetch all required data in parallel, fail fast if any request fails
async function loadDashboard(userId: string): Promise<Result<Dashboard, MapAsyncError>> {
const result = await parallel([
() => fetchUser(userId),
() => fetchUserPosts(userId),
() => fetchUserStats(userId),
() => fetchUserNotifications(userId),
])
return map(result, ([user, posts, stats, notifications]) => ({
user,
posts,
stats,
notifications,
}))
}
// Usage
const dashboardResult = await loadDashboard('user_123')
if (isOk(dashboardResult)) {
renderDashboard(dashboardResult.value)
} else {
showError(dashboardResult.error)
}Pattern: Parallel Operations with Partial Success
import { promiseAllSettled, toResults } from '@ontologyio/receta/async'
import { isOk } from '@ontologyio/receta/result'
// Fetch all data but continue even if some requests fail
async function loadDashboardResilient(
userId: string
): Promise<{
user: User | null
posts: Post[]
stats: Stats | null
notifications: Notification[]
}> {
const settled = await promiseAllSettled([
fetchUser(userId),
fetchUserPosts(userId),
fetchUserStats(userId),
fetchUserNotifications(userId),
])
const results = toResults(settled)
return {
user: isOk(results[0]) ? results[0].value : null,
posts: isOk(results[1]) ? results[1].value : [],
stats: isOk(results[2]) ? results[2].value : null,
notifications: isOk(results[3]) ? results[3].value : [],
}
}
// Usage - never fails!
const dashboard = await loadDashboardResilient('user_123')
renderDashboard(dashboard) // Show what we have, handle nulls gracefullyPattern: Race with First Success
Receta has no race helper. Promise.any does the racing, and tryCatchAsync turns its rejection into an Err:
import { tryCatchAsync } from '@ontologyio/receta/result'
import type { Result } from '@ontologyio/receta/result'
// Try multiple endpoints, use the first one that succeeds
async function fetchFromMirrors<T>(
endpoints: string[]
): Promise<Result<T, string>> {
return tryCatchAsync(
() => Promise.any(
endpoints.map(url => fetch(url).then(r => r.json() as Promise<T>))
),
() => 'All mirror endpoints failed'
)
}
// Usage
const dataResult = await fetchFromMirrors([
'https://primary.api.com/data',
'https://mirror1.api.com/data',
'https://mirror2.api.com/data',
])API Integration Patterns
GitHub API Client with Rate Limiting
GitHub API has strict rate limits (5000 requests/hour authenticated). This pattern handles rate limiting gracefully.
import * as R from 'remeda'
import { isErr, isOk, ok, type Result } from '@ontologyio/receta/result'
import { mapAsync, retry, type RetryError } from '@ontologyio/receta/async'
interface GitHubRateLimit {
limit: number
remaining: number
reset: number
}
interface GitHubRepo {
name: string
full_name: string
stargazers_count: number
language: string
}
class GitHubClient {
private rateLimit: GitHubRateLimit | null = null
private readonly baseUrl = 'https://api.github.com'
constructor(private readonly token: string) {}
/**
* Fetch with automatic rate limit handling
*/
private async fetchWithRateLimit<T>(
endpoint: string
): Promise<Result<T, RetryError>> {
// Check if we're rate limited
if (this.rateLimit && this.rateLimit.remaining === 0) {
const now = Date.now() / 1000
const waitTime = Math.max(0, this.rateLimit.reset - now)
if (waitTime > 0) {
await new Promise(resolve => setTimeout(resolve, waitTime * 1000))
}
}
return retry(
async () => {
const response = await fetch(`${this.baseUrl}${endpoint}`, {
headers: {
'Authorization': `Bearer ${this.token}`,
'Accept': 'application/vnd.github.v3+json',
},
})
// Update rate limit from headers
this.rateLimit = {
limit: parseInt(response.headers.get('X-RateLimit-Limit') || '5000'),
remaining: parseInt(response.headers.get('X-RateLimit-Remaining') || '5000'),
reset: parseInt(response.headers.get('X-RateLimit-Reset') || '0'),
}
if (!response.ok) {
if (response.status === 403 && this.rateLimit.remaining === 0) {
throw new Error('RATE_LIMIT_EXCEEDED')
}
throw new Error(`GitHub API error: ${response.status}`)
}
return await response.json() as T
},
{
maxAttempts: 3,
delay: 1000,
maxDelay: 10000,
shouldRetry: (error) => {
// Retry on rate limit and 5xx errors
const { message } = error as Error
return message === 'RATE_LIMIT_EXCEEDED' || message.includes('5')
},
}
)
}
/**
* Fetch all repositories for an organization
*/
async getOrgRepos(org: string): Promise<Result<GitHubRepo[], RetryError>> {
const repos: GitHubRepo[] = []
let page = 1
while (true) {
const result = await this.fetchWithRateLimit<GitHubRepo[]>(
`/orgs/${org}/repos?page=${page}&per_page=100`
)
if (isErr(result)) {
return result
}
const pageRepos = result.value
if (pageRepos.length === 0) break
repos.push(...pageRepos)
page++
}
return ok(repos)
}
/**
* Fetch multiple repos in parallel with concurrency control
*/
async getReposBatch(repoNames: string[]) {
return mapAsync(
repoNames,
async (name) => this.fetchWithRateLimit<GitHubRepo>(`/repos/${name}`),
{ concurrency: 10 } // Stay under rate limit
)
}
}
// Usage
const client = new GitHubClient(process.env.GITHUB_TOKEN!)
// Fetch all repos for an org
const orgRepos = await client.getOrgRepos('facebook')
if (isOk(orgRepos)) {
console.log(`Found ${orgRepos.value.length} repos`)
const topRepos = R.pipe(
orgRepos.value,
R.sortBy(repo => -repo.stargazers_count),
R.take(10)
)
console.log('Top 10 repos:', topRepos)
}
// Fetch specific repos in parallel
const repos = await client.getReposBatch([
'facebook/react',
'facebook/react-native',
'facebook/jest',
])
if (isOk(repos)) {
const successful = repos.value.filter(isOk)
console.log(`Successfully fetched ${successful.length}/${repos.value.length} repos`)
}Result Version
The example above already uses Result extensively. Key benefits:
- No try-catch blocks needed
- Type-safe error handling with
isOkandisErr - Easy composition with
okanderr - Rate limit errors are values, not exceptions
Stripe Payment Processing
Handle payment processing with proper error handling and retry logic for network failures.
import * as R from 'remeda'
import { err, flatten, isErr, isOk, mapErr, ok, type Result } from '@ontologyio/receta/result'
import { poll, retry, type RetryError } from '@ontologyio/receta/async'
interface StripePaymentIntent {
id: string
amount: number
currency: string
status: 'succeeded' | 'processing' | 'requires_payment_method' | 'canceled'
client_secret: string
}
interface StripeCharge {
id: string
amount: number
paid: boolean
status: string
}
interface CreatePaymentParams {
amount: number
currency: string
customerId: string
description: string
metadata?: Record<string, string>
}
class StripeClient {
private readonly baseUrl = 'https://api.stripe.com/v1'
constructor(private readonly secretKey: string) {}
/**
* Create payment intent with automatic retry
*/
async createPaymentIntent(
params: CreatePaymentParams
): Promise<Result<StripePaymentIntent, RetryError>> {
return retry(
async () => {
const body = new URLSearchParams({
amount: params.amount.toString(),
currency: params.currency,
customer: params.customerId,
description: params.description,
...R.mapValues(params.metadata || {}, String),
})
const response = await fetch(`${this.baseUrl}/payment_intents`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.secretKey}`,
'Content-Type': 'application/x-www-form-urlencoded',
},
body: body.toString(),
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error?.message || 'Payment failed')
}
return await response.json() as StripePaymentIntent
},
{
maxAttempts: 3,
delay: 1000,
shouldRetry: (error) => {
// Retry on network errors, not on payment errors
const { message } = error as Error
return !message.includes('card') && !message.includes('insufficient')
},
}
)
}
/**
* Wait for payment to complete with timeout
*/
async waitForPaymentSuccess(
paymentIntentId: string,
maxWaitMs: number = 30000
): Promise<Result<StripePaymentIntent, string>> {
const finished = await poll(
async () => {
const result = await this.getPaymentIntent(paymentIntentId)
if (isErr(result)) {
return err(`Payment check failed after ${result.error.attempts} attempts`)
}
const intent = result.value
if (intent.status === 'succeeded') {
return ok(intent)
}
if (intent.status === 'canceled' ||
intent.status === 'requires_payment_method') {
return err(`Payment ${intent.status}`)
}
return null // Still processing, check again
},
{
interval: 1000,
maxAttempts: Math.ceil(maxWaitMs / 1000),
timeout: maxWaitMs,
}
)
return flatten(
mapErr(finished, () => `Payment confirmation timeout after ${maxWaitMs}ms`)
)
}
/**
* Get payment intent details
*/
private async getPaymentIntent(
id: string
): Promise<Result<StripePaymentIntent, RetryError>> {
return retry(async () => {
const response = await fetch(`${this.baseUrl}/payment_intents/${id}`, {
headers: {
'Authorization': `Bearer ${this.secretKey}`,
},
})
if (!response.ok) {
throw new Error(`Failed to fetch payment intent: ${response.status}`)
}
return await response.json() as StripePaymentIntent
})
}
/**
* Process refund with validation
*/
async refundPayment(
paymentIntentId: string,
amount?: number
): Promise<Result<StripeCharge, RetryError>> {
return retry(async () => {
const body = new URLSearchParams({
payment_intent: paymentIntentId,
...(amount && { amount: amount.toString() }),
})
const response = await fetch(`${this.baseUrl}/refunds`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.secretKey}`,
'Content-Type': 'application/x-www-form-urlencoded',
},
body: body.toString(),
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error?.message || 'Refund failed')
}
return await response.json() as StripeCharge
})
}
}
// Usage
const stripe = new StripeClient(process.env.STRIPE_SECRET_KEY!)
// Create and wait for payment
const paymentResult = await stripe.createPaymentIntent({
amount: 2000, // $20.00
currency: 'usd',
customerId: 'cus_123',
description: 'Premium subscription',
metadata: {
orderId: 'order_456',
userId: 'user_789',
},
})
if (isOk(paymentResult)) {
const intent = paymentResult.value
console.log(`Payment intent created: ${intent.id}`)
// Wait for payment to complete
const completedResult = await stripe.waitForPaymentSuccess(intent.id)
if (isOk(completedResult)) {
console.log('Payment succeeded!')
} else {
console.error('Payment failed:', completedResult.error)
// Optionally refund
await stripe.refundPayment(intent.id)
}
}Result Version
The Stripe example already uses Result for type-safe payment processing:
- Payment failures return
errinstead of throwing waitForPaymentSuccessusespollfor safe polling with a time limit- Refunds use
retryto handle transient failures - All operations compose cleanly with pipes and
isOk
AWS S3 File Operations
Upload and download files from S3 with proper error handling.
import * as R from 'remeda'
import { isErr, isOk, ok, type Result } from '@ontologyio/receta/result'
import { mapAsync, retry, type RetryError } from '@ontologyio/receta/async'
import { createHash } from 'crypto'
interface S3Object {
key: string
size: number
lastModified: Date
etag: string
}
interface UploadOptions {
contentType?: string
metadata?: Record<string, string>
}
class S3Client {
private readonly baseUrl: string
constructor(
private readonly accessKeyId: string,
private readonly secretAccessKey: string,
private readonly bucket: string,
private readonly region: string = 'us-east-1'
) {
this.baseUrl = `https://${bucket}.s3.${region}.amazonaws.com`
}
/**
* Upload file with automatic retry and chunking for large files
*/
async uploadFile(
key: string,
content: Buffer | Blob,
options: UploadOptions = {}
): Promise<Result<S3Object, RetryError>> {
const contentBuffer = content instanceof Blob
? Buffer.from(await content.arrayBuffer())
: content
const contentLength = contentBuffer.length
const contentMD5 = createHash('md5').update(contentBuffer).digest('base64')
return retry(
async () => {
const response = await fetch(`${this.baseUrl}/${key}`, {
method: 'PUT',
headers: {
'Content-Type': options.contentType || 'application/octet-stream',
'Content-Length': contentLength.toString(),
'Content-MD5': contentMD5,
...this.getAuthHeaders('PUT', key),
...R.mapKeys(options.metadata || {}, k => `x-amz-meta-${k}`),
},
body: contentBuffer,
})
if (!response.ok) {
throw new Error(`S3 upload failed: ${response.status}`)
}
const etag = response.headers.get('ETag')?.replace(/"/g, '') || ''
return {
key,
size: contentLength,
lastModified: new Date(),
etag,
}
},
{
maxAttempts: 3,
delay: 1000,
shouldRetry: (error) => {
// Retry on network errors, not on auth errors
const { message } = error as Error
return !message.includes('403') && !message.includes('401')
},
}
)
}
/**
* Download file with retry
*/
async downloadFile(key: string): Promise<Result<Buffer, RetryError>> {
return retry(async () => {
const response = await fetch(`${this.baseUrl}/${key}`, {
headers: this.getAuthHeaders('GET', key),
})
if (!response.ok) {
throw new Error(`S3 download failed: ${response.status}`)
}
const arrayBuffer = await response.arrayBuffer()
return Buffer.from(arrayBuffer)
})
}
/**
* Upload multiple files in parallel
*/
async uploadBatch(
files: Array<{ key: string; content: Buffer; options?: UploadOptions }>,
concurrency: number = 5
) {
return mapAsync(
files,
async (file) => this.uploadFile(file.key, file.content, file.options),
{ concurrency }
)
}
/**
* List objects with pagination
*/
async listObjects(prefix?: string): Promise<Result<S3Object[], RetryError>> {
const objects: S3Object[] = []
let continuationToken: string | undefined
while (true) {
const url = new URL(this.baseUrl)
url.searchParams.set('list-type', '2')
if (prefix) url.searchParams.set('prefix', prefix)
if (continuationToken) {
url.searchParams.set('continuation-token', continuationToken)
}
const result = await retry(async () => {
const response = await fetch(url.toString(), {
headers: this.getAuthHeaders('GET', '/'),
})
if (!response.ok) {
throw new Error(`S3 list failed: ${response.status}`)
}
return await response.text()
})
if (isErr(result)) {
return result
}
// Parse XML response (simplified - use xml2js in production)
const xmlText = result.value
const keyMatches = xmlText.matchAll(/<Key>(.*?)<\/Key>/g)
const sizeMatches = xmlText.matchAll(/<Size>(.*?)<\/Size>/g)
const pageObjects = Array.from(keyMatches).map((match, i) => ({
key: match[1],
size: parseInt(Array.from(sizeMatches)[i]?.[1] || '0'),
lastModified: new Date(),
etag: '',
}))
objects.push(...pageObjects)
// Check for more pages
const isTruncated = xmlText.includes('<IsTruncated>true</IsTruncated>')
if (!isTruncated) break
const tokenMatch = xmlText.match(/<NextContinuationToken>(.*?)<\/NextContinuationToken>/)
continuationToken = tokenMatch?.[1]
if (!continuationToken) break
}
return ok(objects)
}
/**
* Generate signed auth headers (simplified AWS Signature V4)
*/
private getAuthHeaders(method: string, path: string): Record<string, string> {
// In production, use aws-sdk or proper signature generation
// This is a placeholder
return {
'Authorization': `AWS ${this.accessKeyId}:signature`,
'x-amz-date': new Date().toISOString(),
}
}
}
// Usage
const s3 = new S3Client(
process.env.AWS_ACCESS_KEY_ID!,
process.env.AWS_SECRET_ACCESS_KEY!,
'my-bucket',
'us-east-1'
)
// Upload single file
const uploadResult = await s3.uploadFile(
'uploads/image.png',
Buffer.from('...'),
{
contentType: 'image/png',
metadata: {
userId: '123',
originalName: 'photo.png',
},
}
)
if (isOk(uploadResult)) {
console.log('Upload successful:', uploadResult.value)
}
// Upload multiple files
const files = [
{ key: 'file1.txt', content: Buffer.from('content1') },
{ key: 'file2.txt', content: Buffer.from('content2') },
{ key: 'file3.txt', content: Buffer.from('content3') },
]
const results = await s3.uploadBatch(files, 3)
if (isOk(results)) {
const successful = results.value.filter(isOk)
console.log(`Uploaded ${successful.length}/${files.length} files`)
}
// List and download
const listResult = await s3.listObjects('uploads/')
if (isOk(listResult)) {
const images = listResult.value.filter(obj => obj.key.endsWith('.png'))
const downloads = await mapAsync(
images,
async (obj) => s3.downloadFile(obj.key),
{ concurrency: 5 }
)
if (isOk(downloads)) {
console.log(`Downloaded ${downloads.value.filter(isOk).length} images`)
}
}Result Version
The S3 example demonstrates Result-based file operations:
- Upload/download operations return
Result<S3Object, RetryError>instead of throwing uploadBatchusesmapAsyncto process files with concurrency control- All network errors become Result errors for consistent handling
- Batch operations use
filter(isOk)to count successes
Bulk Operations
User Import/Export
Handle large-scale user data imports with validation and error recovery.
import * as R from 'remeda'
import { err, isErr, isOk, ok, type Result } from '@ontologyio/receta/result'
import { mapAsync } from '@ontologyio/receta/async'
interface UserRecord {
email: string
name: string
role: 'admin' | 'user' | 'guest'
metadata?: Record<string, unknown>
}
interface ImportResult {
total: number
successful: number
failed: number
errors: Array<{ row: number; email: string; error: string }>
}
/**
* Import users from CSV with validation and batch processing
*/
async function importUsers(
csvContent: string,
options: {
batchSize?: number
concurrency?: number
onProgress?: (processed: number, total: number) => void
} = {}
): Promise<Result<ImportResult, string>> {
const { batchSize = 100, concurrency = 5, onProgress } = options
// Parse CSV
const lines = csvContent.trim().split('\n')
const headers = lines[0].split(',')
const rows = lines.slice(1)
// Validate headers
const requiredHeaders = ['email', 'name', 'role']
const missingHeaders = requiredHeaders.filter(h => !headers.includes(h))
if (missingHeaders.length > 0) {
return err(`Missing required headers: ${missingHeaders.join(', ')}`)
}
// Parse rows
const users = rows.map((line, idx) => {
const values = line.split(',')
const user: Record<string, string> = {}
headers.forEach((header, i) => {
user[header] = values[i]?.trim() || ''
})
return { row: idx + 2, user } // +2 for header and 1-indexed
})
const result: ImportResult = {
total: users.length,
successful: 0,
failed: 0,
errors: [],
}
// Process in batches
const batches = R.chunk(users, batchSize)
for (const batch of batches) {
const results = await mapAsync(
batch,
async ({ row, user }) => {
// Validate user
const validation = validateUser(user)
if (isErr(validation)) {
return { row, email: user.email, error: validation.error }
}
// Create user in database
const createResult = await createUser(validation.value)
if (isErr(createResult)) {
return { row, email: user.email, error: createResult.error }
}
return { row, email: user.email, success: true }
},
{ concurrency }
)
// The mapper reports failures as values, so this only catches a thrown error
if (isErr(results)) {
return err('Import stopped by an unexpected error')
}
// Collect results
for (const res of results.value) {
if ('success' in res) {
result.successful++
} else {
result.failed++
result.errors.push(res)
}
}
// Report progress
if (onProgress) {
const processed = result.successful + result.failed
onProgress(processed, result.total)
}
}
return ok(result)
}
/**
* Validate user record
*/
function validateUser(
user: Record<string, string>
): Result<UserRecord, string> {
// Email validation
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!emailRegex.test(user.email)) {
return err(`Invalid email: ${user.email}`)
}
// Name validation
if (!user.name || user.name.length < 2) {
return err('Name must be at least 2 characters')
}
// Role validation
const validRoles = ['admin', 'user', 'guest']
if (!validRoles.includes(user.role)) {
return err(`Invalid role: ${user.role}`)
}
return ok({
email: user.email,
name: user.name,
role: user.role as UserRecord['role'],
metadata: R.omit(user, ['email', 'name', 'role']),
})
}
/**
* Create user in database (mock)
*/
async function createUser(user: UserRecord): Promise<Result<void, string>> {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 100))
// Simulate occasional failures
if (Math.random() < 0.05) {
return err('Database error')
}
return ok(undefined)
}
// Usage
const csv = `email,name,role,department
john@example.com,John Doe,admin,Engineering
jane@example.com,Jane Smith,user,Sales
invalid-email,Bad User,admin,Engineering
bob@example.com,Bob Johnson,user,Marketing`
const importResult = await importUsers(csv, {
batchSize: 50,
concurrency: 10,
onProgress: (processed, total) => {
console.log(`Progress: ${processed}/${total} (${(processed/total*100).toFixed(1)}%)`)
},
})
if (isOk(importResult)) {
const stats = importResult.value
console.log(`Import complete:
Total: ${stats.total}
Successful: ${stats.successful}
Failed: ${stats.failed}
`)
if (stats.errors.length > 0) {
console.log('\nErrors:')
stats.errors.forEach(err => {
console.log(` Row ${err.row} (${err.email}): ${err.error}`)
})
}
}Result Version
The User Import example shows comprehensive Result usage:
- CSV validation returns
errfor malformed data - Each row import returns
Result<void, string>for tracking failures - Validation errors are collected without stopping the entire import
- Progress reporting works seamlessly with Result-based error tracking
- Final statistics aggregate successes and failures from Result values
Data Synchronization
Sync data between systems with conflict resolution and incremental updates.
import * as R from 'remeda'
import { err, isErr, isOk, ok, type Result } from '@ontologyio/receta/result'
import { mapAsync } from '@ontologyio/receta/async'
interface SyncRecord {
id: string
data: unknown
version: number
updatedAt: Date
}
interface SyncResult {
created: number
updated: number
deleted: number
conflicts: number
errors: Array<{ id: string; error: string }>
}
/**
* Sync records between source and destination
*/
async function syncData(
sourceRecords: SyncRecord[],
destinationRecords: SyncRecord[],
options: {
conflictResolution?: 'source' | 'destination' | 'newest'
concurrency?: number
} = {}
): Promise<Result<SyncResult, string>> {
const { conflictResolution = 'newest', concurrency = 10 } = options
const result: SyncResult = {
created: 0,
updated: 0,
deleted: 0,
conflicts: 0,
errors: [],
}
// Index destination records
const destById = R.indexBy(destinationRecords, r => r.id)
const sourceById = R.indexBy(sourceRecords, r => r.id)
// Find records to create, update, and delete
const toCreate: SyncRecord[] = []
const toUpdate: Array<{ source: SyncRecord; dest: SyncRecord }> = []
const toDelete: SyncRecord[] = []
// Check source records
for (const sourceRecord of sourceRecords) {
const destRecord = destById[sourceRecord.id]
if (!destRecord) {
toCreate.push(sourceRecord)
} else if (sourceRecord.version !== destRecord.version) {
toUpdate.push({ source: sourceRecord, dest: destRecord })
}
}
// Check for deletions
for (const destRecord of destinationRecords) {
if (!sourceById[destRecord.id]) {
toDelete.push(destRecord)
}
}
// Process creates
const createResults = await mapAsync(
toCreate,
async (record) => {
const res = await createRecord(record)
return { id: record.id, result: res }
},
{ concurrency }
)
if (isErr(createResults)) return err('Sync stopped while creating records')
for (const { id, result: res } of createResults.value) {
if (isOk(res)) {
result.created++
} else {
result.errors.push({ id, error: res.error })
}
}
// Process updates with conflict resolution
const updateResults = await mapAsync(
toUpdate,
async ({ source, dest }) => {
// Detect conflicts (both modified since last sync)
const isConflict = source.version > dest.version + 1
if (isConflict) {
result.conflicts++
}
// Resolve conflict
let recordToUse: SyncRecord
if (conflictResolution === 'source') {
recordToUse = source
} else if (conflictResolution === 'destination') {
recordToUse = dest
} else {
// Use newest
recordToUse = source.updatedAt > dest.updatedAt ? source : dest
}
const res = await updateRecord(recordToUse)
return { id: source.id, result: res }
},
{ concurrency }
)
if (isErr(updateResults)) return err('Sync stopped while updating records')
for (const { id, result: res } of updateResults.value) {
if (isOk(res)) {
result.updated++
} else {
result.errors.push({ id, error: res.error })
}
}
// Process deletes
const deleteResults = await mapAsync(
toDelete,
async (record) => {
const res = await deleteRecord(record.id)
return { id: record.id, result: res }
},
{ concurrency }
)
if (isErr(deleteResults)) return err('Sync stopped while deleting records')
for (const { id, result: res } of deleteResults.value) {
if (isOk(res)) {
result.deleted++
} else {
result.errors.push({ id, error: res.error })
}
}
return ok(result)
}
// Mock operations
async function createRecord(record: SyncRecord): Promise<Result<void, string>> {
await new Promise(resolve => setTimeout(resolve, 50))
return ok(undefined)
}
async function updateRecord(record: SyncRecord): Promise<Result<void, string>> {
await new Promise(resolve => setTimeout(resolve, 50))
return ok(undefined)
}
async function deleteRecord(id: string): Promise<Result<void, string>> {
await new Promise(resolve => setTimeout(resolve, 50))
return ok(undefined)
}
// Usage
const sourceRecords: SyncRecord[] = [
{ id: '1', data: { name: 'A' }, version: 2, updatedAt: new Date('2024-01-15') },
{ id: '2', data: { name: 'B' }, version: 1, updatedAt: new Date('2024-01-14') },
{ id: '3', data: { name: 'C' }, version: 1, updatedAt: new Date('2024-01-16') },
]
const destRecords: SyncRecord[] = [
{ id: '1', data: { name: 'A-old' }, version: 1, updatedAt: new Date('2024-01-10') },
{ id: '2', data: { name: 'B' }, version: 1, updatedAt: new Date('2024-01-14') },
{ id: '4', data: { name: 'D' }, version: 1, updatedAt: new Date('2024-01-12') },
]
const syncResult = await syncData(sourceRecords, destRecords, {
conflictResolution: 'newest',
concurrency: 5,
})
if (isOk(syncResult)) {
console.log('Sync complete:', syncResult.value)
}Result Version
The Data Sync example uses Result for safe synchronization:
- Create/update/delete operations return
Result<void, string> - Conflict detection and resolution happen without exceptions
- Errors are accumulated in a structured format
- The final sync result aggregates all operation results
- Type-safe error handling ensures no sync failures are missed
Batch Email Sending
Send emails in batches with rate limiting and failure tracking.
import * as R from 'remeda'
import { err, isErr, isOk, ok, type Result } from '@ontologyio/receta/result'
import { mapAsync } from '@ontologyio/receta/async'
interface Email {
to: string
subject: string
body: string
attachments?: Array<{ filename: string; content: Buffer }>
}
interface SendResult {
email: string
messageId?: string
error?: string
}
/**
* Send emails in batches with rate limiting
*/
async function sendEmailBatch(
emails: Email[],
options: {
batchSize?: number
delayBetweenBatches?: number
maxRetriesPerEmail?: number
} = {}
): Promise<Result<SendResult[], string>> {
const {
batchSize = 50,
delayBetweenBatches = 1000,
maxRetriesPerEmail = 3,
} = options
const results: SendResult[] = []
const batches = R.chunk(emails, batchSize)
for (const [batchIndex, batch] of batches.entries()) {
console.log(`Processing batch ${batchIndex + 1}/${batches.length}`)
// Send batch in parallel
const batchResults = await mapAsync(
batch,
async (email) => {
let lastError: string | undefined
// Retry logic
for (let attempt = 1; attempt <= maxRetriesPerEmail; attempt++) {
const result = await sendEmail(email)
if (isOk(result)) {
return {
email: email.to,
messageId: result.value,
}
}
lastError = result.error
if (attempt < maxRetriesPerEmail) {
// Wait before retry
await new Promise(resolve =>
setTimeout(resolve, 1000 * attempt)
)
}
}
return {
email: email.to,
error: lastError,
}
},
{ concurrency: 10 } // Limit concurrent sends
)
if (isErr(batchResults)) {
return err(`Batch ${batchIndex + 1} stopped by an unexpected error`)
}
results.push(...batchResults.value)
// Delay between batches to respect rate limits
if (batchIndex < batches.length - 1) {
await new Promise(resolve => setTimeout(resolve, delayBetweenBatches))
}
}
return ok(results)
}
/**
* Send single email (mock implementation)
*/
async function sendEmail(email: Email): Promise<Result<string, string>> {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 100))
// Simulate occasional failures
if (Math.random() < 0.1) {
return err('SMTP error: Connection timeout')
}
return ok(`msg-${Date.now()}-${Math.random()}`)
}
// Usage
const emails: Email[] = Array.from({ length: 200 }, (_, i) => ({
to: `user${i}@example.com`,
subject: 'Welcome!',
body: `Hello user ${i}!`,
}))
const result = await sendEmailBatch(emails, {
batchSize: 50,
delayBetweenBatches: 1000,
maxRetriesPerEmail: 3,
})
if (isOk(result)) {
const successful = result.value.filter(r => r.messageId)
const failed = result.value.filter(r => r.error)
console.log(`Email sending complete:
Total: ${result.value.length}
Successful: ${successful.length}
Failed: ${failed.length}
`)
if (failed.length > 0) {
console.log('\nFailed emails:')
failed.forEach(r => {
console.log(` ${r.email}: ${r.error}`)
})
}
}Result Version
The Batch Email example demonstrates Result for email campaigns:
- Each email send returns
Result<string, string>with message ID or error - Retry logic is built into the Result flow without try-catch
- Success/failure tracking is type-safe and explicit
- Final report aggregates Result values for statistics
- No silent failures - every error is captured and reported
Real-time Updates
Webhook Delivery
Deliver webhooks with retry and exponential backoff.
import { isOk, ok, type Result } from '@ontologyio/receta/result'
import { retry, type RetryError } from '@ontologyio/receta/async'
interface WebhookEvent {
id: string
type: string
data: unknown
timestamp: Date
}
interface WebhookEndpoint {
url: string
secret: string
events: string[]
}
/**
* Deliver webhook with retry and signature
*/
async function deliverWebhook(
endpoint: WebhookEndpoint,
event: WebhookEvent
): Promise<Result<void, RetryError>> {
// Check if endpoint is subscribed to this event
if (!endpoint.events.includes(event.type)) {
return ok(undefined) // Skip delivery
}
const payload = JSON.stringify(event)
const signature = await generateSignature(payload, endpoint.secret)
return retry(
async () => {
const response = await fetch(endpoint.url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Webhook-Signature': signature,
'X-Webhook-ID': event.id,
'X-Webhook-Timestamp': event.timestamp.toISOString(),
},
body: payload,
})
if (!response.ok) {
throw new Error(`Webhook delivery failed: ${response.status}`)
}
return undefined
},
{
maxAttempts: 5,
delay: 1000,
maxDelay: 60000,
backoff: 2,
shouldRetry: (error) => {
// Retry on 5xx errors and timeouts, not on 4xx
return !(error as Error).message.includes('4')
},
}
)
}
/**
* Generate HMAC signature for webhook
*/
async function generateSignature(
payload: string,
secret: string
): Promise<string> {
const encoder = new TextEncoder()
const keyData = encoder.encode(secret)
const messageData = encoder.encode(payload)
const key = await crypto.subtle.importKey(
'raw',
keyData,
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
)
const signature = await crypto.subtle.sign('HMAC', key, messageData)
const hashArray = Array.from(new Uint8Array(signature))
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('')
}
// Usage
const endpoint: WebhookEndpoint = {
url: 'https://api.example.com/webhooks',
secret: 'whsec_...',
events: ['user.created', 'user.updated'],
}
const event: WebhookEvent = {
id: 'evt_123',
type: 'user.created',
data: {
userId: '123',
email: 'user@example.com',
},
timestamp: new Date(),
}
const result = await deliverWebhook(endpoint, event)
if (isOk(result)) {
console.log('Webhook delivered successfully')
} else {
console.error('Webhook delivery failed:', result.error)
}Result Version
The Webhook Delivery example shows Result for reliable delivery:
deliverWebhookreturnsResult<void, RetryError>for explicit success/failureretrywraps network calls with Result-based retry logic- Signature generation and validation errors are Result values
- Webhook failures are logged without throwing exceptions
- Easy to track delivery success rates with Result filtering
Job Status Polling
Poll for job completion with timeout and progress updates.
import { err, flatten, isErr, isOk, mapErr, ok, type Result } from '@ontologyio/receta/result'
import { poll } from '@ontologyio/receta/async'
interface Job {
id: string
status: 'pending' | 'running' | 'completed' | 'failed'
progress?: number
result?: unknown
error?: string
}
/**
* Poll job status until completion
*/
async function waitForJobCompletion(
jobId: string,
options: {
pollInterval?: number
maxWaitTime?: number
onProgress?: (job: Job) => void
} = {}
): Promise<Result<Job, string>> {
const {
pollInterval = 1000,
maxWaitTime = 300000, // 5 minutes
onProgress,
} = options
const finished = await poll(
async () => {
const jobResult = await fetchJobStatus(jobId)
if (isErr(jobResult)) {
return jobResult
}
const job = jobResult.value
// Report progress
if (onProgress) {
onProgress(job)
}
// Check terminal states
if (job.status === 'completed') {
return ok(job)
}
if (job.status === 'failed') {
return err(job.error || 'Job failed')
}
return null // Not done yet, poll again
},
{
interval: pollInterval,
maxAttempts: Math.ceil(maxWaitTime / pollInterval),
timeout: maxWaitTime,
}
)
return flatten(
mapErr(finished, () => `Job did not complete within ${maxWaitTime}ms`)
)
}
/**
* Fetch job status (mock)
*/
async function fetchJobStatus(jobId: string): Promise<Result<Job, string>> {
await new Promise(resolve => setTimeout(resolve, 100))
// Simulate job progress
const progress = Math.min(100, (Date.now() % 10000) / 100)
const status = progress < 100 ? 'running' : 'completed'
return ok({
id: jobId,
status,
progress,
result: status === 'completed' ? { data: 'result' } : undefined,
})
}
// Usage
const jobId = 'job_123'
const result = await waitForJobCompletion(jobId, {
pollInterval: 2000,
maxWaitTime: 60000,
onProgress: (job) => {
console.log(`Job progress: ${job.progress?.toFixed(1)}%`)
},
})
if (isOk(result)) {
console.log('Job completed:', result.value.result)
} else {
console.error('Job failed:', result.error)
}Result Version
The Job Polling example uses Result for async job tracking:
waitForJobCompletionreturnsResult<Job, string>when complete or failedpollruns the loop and enforces the time limit- Job status checks use
isErrfor early termination - Progress callbacks work seamlessly with Result-based flow
- Terminal states (completed/failed) are distinguished by Result value
Error Recovery
Circuit Breaker Pattern
Prevent cascading failures with circuit breaker.
import { err, isErr, ok, type Result } from '@ontologyio/receta/result'
type CircuitState = 'closed' | 'open' | 'half-open'
interface CircuitBreakerOptions {
failureThreshold: number
successThreshold: number
timeout: number
}
class CircuitBreaker {
private state: CircuitState = 'closed'
private failureCount = 0
private successCount = 0
private nextAttempt = Date.now()
constructor(private options: CircuitBreakerOptions) {}
async execute<T>(
fn: () => Promise<T>
): Promise<Result<T, string>> {
if (this.state === 'open') {
if (Date.now() < this.nextAttempt) {
return err('Circuit breaker is open')
}
this.state = 'half-open'
}
try {
const result = await fn()
this.onSuccess()
return ok(result)
} catch (error) {
this.onFailure()
return err(error instanceof Error ? error.message : 'Unknown error')
}
}
private onSuccess(): void {
this.failureCount = 0
if (this.state === 'half-open') {
this.successCount++
if (this.successCount >= this.options.successThreshold) {
this.state = 'closed'
this.successCount = 0
}
}
}
private onFailure(): void {
this.failureCount++
this.successCount = 0
if (this.failureCount >= this.options.failureThreshold) {
this.state = 'open'
this.nextAttempt = Date.now() + this.options.timeout
}
}
getState(): CircuitState {
return this.state
}
}
// Usage
const breaker = new CircuitBreaker({
failureThreshold: 5,
successThreshold: 2,
timeout: 60000,
})
async function callExternalAPI(): Promise<string> {
const result = await breaker.execute(async () => {
const response = await fetch('https://api.example.com/data')
if (!response.ok) throw new Error('API error')
return await response.text()
})
if (isErr(result)) {
console.log('Circuit breaker state:', breaker.getState())
return 'fallback data'
}
return result.value
}Result Version
The Circuit Breaker example demonstrates Result for fault tolerance:
executemethod returnsResult<T, string>for all operations- Circuit state transitions are based on Result success/failure
- Open circuit returns
errimmediately without calling function - Half-open state tests recovery using Result-based success tracking
- Fallback logic is explicit and type-safe with Result pattern
Performance Optimization
Concurrency Tuning
Find optimal concurrency for your workload.
import { mapAsync } from '@ontologyio/receta/async'
/**
* Benchmark different concurrency levels
*/
async function findOptimalConcurrency<T, U>(
items: T[],
fn: (item: T) => Promise<U>,
concurrencyLevels: number[] = [1, 5, 10, 20, 50, 100]
): Promise<{ concurrency: number; duration: number }> {
const results: Array<{ concurrency: number; duration: number }> = []
for (const concurrency of concurrencyLevels) {
const start = Date.now()
await mapAsync(items, fn, { concurrency })
const duration = Date.now() - start
results.push({ concurrency, duration })
console.log(`Concurrency ${concurrency}: ${duration}ms`)
}
// Find fastest
return results.reduce((best, current) =>
current.duration < best.duration ? current : best
)
}
// Usage
const urls = Array.from({ length: 100 }, (_, i) =>
`https://api.example.com/data/${i}`
)
const optimal = await findOptimalConcurrency(
urls,
async (url) => {
const response = await fetch(url)
return await response.json()
}
)
console.log(`Optimal concurrency: ${optimal.concurrency}`)Summary: Why Use Result Patterns?
The patterns in this document demonstrate the power of Result-based async operations:
Benefits
- No try-catch blocks: Errors are values that compose naturally
- Type-safe error handling: TypeScript ensures you handle all error cases
- Explicit error propagation: See exactly where errors can occur
- Easy composition: Chain operations with
pipe,map,flatMap - Consistent API:
retry,mapAsync,pollandbatchall return a Result - Better testing: Mock errors by returning
errvalues - No silent failures: All errors are explicit and tracked
When to Use Result vs Exceptions
Use Result when:
- Building API clients with expected failures (rate limits, 404s, etc.)
- Processing batches where some items may fail
- Implementing retry logic with multiple failure modes
- Need to collect errors without stopping execution
- Want explicit error handling in function signatures
Use exceptions when:
- Truly unexpected errors (bugs, programmer errors)
- Need to abort execution immediately
- Working with libraries that expect exception handling
- Validating function preconditions
Migration Path
To migrate existing try-catch code to Result:
// Before: try-catch
async function fetchUser(id: string): Promise<User> {
try {
const response = await fetch(`/users/${id}`)
if (!response.ok) throw new Error('Not found')
return await response.json()
} catch (error) {
console.error('Failed to fetch user:', error)
throw error
}
}
// After: Result
async function fetchUser(id: string): Promise<Result<User, RetryError>> {
return retry(async () => {
const response = await fetch(`/users/${id}`)
if (!response.ok) throw new Error('Not found')
return await response.json()
}, { maxAttempts: 3 })
}This document provides production-ready patterns for common async operations. All examples use real APIs and include proper error handling, retry logic, and performance optimization.