--- 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-09-04" 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 response = await fetch(url, { headers: { 'Content-Type': 'application/json', ...options.headers, }, ...options, }) if (!response.ok) { const body = await response.json().catch(() => ({})) throw createApiError( body.code || 'HTTP_ERROR', body.message || response.statusText, response.status, body ) } // Handle 204 No Content if (response.status === 204) { return undefined as T } return response.json() }, (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/${id}`) const createUser = (data: CreateUserDto) => api.post('/api/users', data) const updateUser = (id: string, data: UpdateUserDto) => api.put(`/api/users/${id}`, data) const deleteUser = (id: string) => api.delete(`/api/users/${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 field = (error.meta?.target as string[])?.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.chain(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.