--- name: fp-async description: Practical async patterns using TaskEither - clean pipelines instead of try/catch hell, with real API examples risk: critical source: community date_added: "2026-03-06" version: 1.0.0 author: kadu tags: - fp-ts - typescript - async - error-handling - practical - promises - api - fetch --- # Practical Async Patterns with fp-ts Stop writing nested try/catch blocks. Stop losing error context. Start building clean async pipelines that handle errors properly. **TaskEither is simply an async operation that tracks success or failure.** That's it. No fancy terminology needed. ## Detailed Guide Read [the detailed guide](references/detailed-guide.md) before executing this skill. It retains the complete procedure and reference material. Treat its safety, prerequisites, and validation requirements as mandatory. For focused work, load the relevant sections; for end-to-end work, read the guide completely. ## When to Use - You need async error handling in TypeScript with `TaskEither`. - The task involves wrapping Promises, composing API calls, or replacing nested `try/catch` flows. - You want practical fp-ts async patterns instead of academic explanations. ```typescript // TaskEither means: // "An async operation that either fails with Error or succeeds with User" ``` --- ## 5. Real API Examples ### Complete Fetch Wrapper ```typescript // types.ts interface ApiError { code: string message: string status: number details?: unknown } // api.ts const createApiError = ( code: string, message: string, status: number, details?: unknown ): ApiError => ({ code, message, status, details }) const request = ( url: string, options: RequestInit = {} ): TE.TaskEither => TE.tryCatch( async () => { const headers = new Headers(options.headers) if (options.body != null && !headers.has('Content-Type')) headers.set('Content-Type', 'application/json') const response = await fetch(url, { ...options, headers, signal: options.signal ?? AbortSignal.timeout(10000), }) if (!response.ok) { const parsed: unknown = await response.json().catch(() => ({})) const body = typeof parsed === 'object' && parsed !== null ? parsed as Record : {} throw createApiError( typeof body.code === 'string' ? body.code : 'HTTP_ERROR', typeof body.message === 'string' ? body.message : response.statusText, response.status, body ) } // Handle 204 No Content if (response.status === 204) { throw createApiError("EMPTY_RESPONSE", "Expected JSON; use a separate void decoder for 204", 204) } return response.json() // Decode unknown with a runtime schema before trusting T }, (error): ApiError => { if (typeof error === 'object' && error !== null && 'code' in error) { return error as ApiError } return createApiError( 'NETWORK_ERROR', error instanceof Error ? error.message : 'Request failed', 0 ) } ) // API client const api = { get: (url: string) => request(url), post: (url: string, body: unknown) => request(url, { method: 'POST', body: JSON.stringify(body) }), put: (url: string, body: unknown) => request(url, { method: 'PUT', body: JSON.stringify(body) }), delete: (url: string) => request(url, { method: 'DELETE' }), } // Usage const getUser = (id: string) => api.get(`/api/users/${encodeURIComponent(id)}`) const createUser = (data: CreateUserDto) => api.post('/api/users', data) const updateUser = (id: string, data: UpdateUserDto) => api.put(`/api/users/${encodeURIComponent(id)}`, data) const deleteUser = (id: string) => api.delete(`/api/users/${encodeURIComponent(id)}`) ``` ### Database Operations (Prisma Example) ```typescript import { PrismaClient, Prisma } from '@prisma/client' type DbError = | { _tag: 'NotFound'; entity: string; id: string } | { _tag: 'UniqueViolation'; field: string } | { _tag: 'ConnectionError'; cause: unknown } const prisma = new PrismaClient() const wrapPrisma = ( operation: () => Promise ): TE.TaskEither => TE.tryCatch( operation, (error): DbError => { if (error instanceof Prisma.PrismaClientKnownRequestError) { if (error.code === 'P2002') { const target = error.meta?.target const field = Array.isArray(target) && target.every(x => typeof x === 'string') ? target.join(', ') : 'unknown' return { _tag: 'UniqueViolation', field } } if (error.code === 'P2025') { return { _tag: 'NotFound', entity: 'Record', id: 'unknown' } } } return { _tag: 'ConnectionError', cause: error } } ) // Repository pattern const userRepository = { findById: (id: string): TE.TaskEither => pipe( wrapPrisma(() => prisma.user.findUnique({ where: { id } })), TE.chain(user => user ? TE.right(user) : TE.left({ _tag: 'NotFound', entity: 'User', id }) ) ), findByEmail: (email: string): TE.TaskEither => wrapPrisma(() => prisma.user.findUnique({ where: { email } })), create: (data: CreateUserInput): TE.TaskEither => wrapPrisma(() => prisma.user.create({ data })), update: (id: string, data: UpdateUserInput): TE.TaskEither => wrapPrisma(() => prisma.user.update({ where: { id }, data })), delete: (id: string): TE.TaskEither => pipe( wrapPrisma(() => prisma.user.delete({ where: { id } })), TE.map(() => undefined) ), } // Service using repository const createUserService = (input: CreateUserInput) => pipe( // Check email doesn't exist userRepository.findByEmail(input.email), TE.chain(existing => existing ? TE.left({ _tag: 'UniqueViolation' as const, field: 'email' }) : TE.right(undefined) ), // Create user TE.chain(() => userRepository.create(input)) ) ``` ### File Operations (Node.js) ```typescript import * as fs from 'fs/promises' import * as path from 'path' type FileError = | { _tag: 'NotFound'; path: string } | { _tag: 'PermissionDenied'; path: string } | { _tag: 'IoError'; cause: unknown } const toFileError = (error: unknown, filePath: string): FileError => { if (error instanceof Error) { if ('code' in error) { if (error.code === 'ENOENT') return { _tag: 'NotFound', path: filePath } if (error.code === 'EACCES') return { _tag: 'PermissionDenied', path: filePath } } } return { _tag: 'IoError', cause: error } } const readFile = (filePath: string): TE.TaskEither => TE.tryCatch( () => fs.readFile(filePath, 'utf-8'), (e) => toFileError(e, filePath) ) const writeFile = (filePath: string, content: string): TE.TaskEither => TE.tryCatch( () => fs.writeFile(filePath, content, 'utf-8'), (e) => toFileError(e, filePath) ) const readJson = (filePath: string): TE.TaskEither => pipe( readFile(filePath), TE.chainW(content => TE.tryCatch( () => Promise.resolve(JSON.parse(content)), (e): { _tag: 'ParseError'; cause: unknown } => ({ _tag: 'ParseError', cause: e }) ) ) ) // Usage: Load config with fallback const loadConfig = () => pipe( readJson('./config.json'), TE.orElse(() => readJson('./config.default.json')), TE.getOrElse(() => T.of(defaultConfig)) ) ``` --- ## Limitations - Use this skill only when the task clearly matches the scope described above. - Do not treat the output as a substitute for environment-specific validation, testing, or expert review. - Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing. ## Execution and data boundaries These are separate fp-ts 2.x teaching fragments, with omitted imports, domain types, runtime decoders and application helpers. TaskEither is lazy: calling the returned task again repeats effects, and a pre-created Promise is not a fresh retry. The corrected batch traversal is sequential between batches; parallel traversal does not cancel in-flight work. Validate unknown JSON instead of asserting a generic T, and handle no-content responses through an explicit void decoder. A pipeline is not a database transaction or payment saga: preserve inventory checks, use durable/provider idempotency, and reconcile failed shipment or ambiguous charge outcomes. Keep uniqueness constraints even after a pre-check. Retry only classified transient, safely repeatable operations with an overall deadline and jitter; the generic example retries every Left. Do not recover authorization, malformed security configuration or invalid data with defaults; branch on typed status rather than message substrings. File paths need an authorized root, and writes currently overwrite existing files. Map errors to safe public status/messages without serializing raw causes or database details.