fp-ts-task-either

fp-ts TaskEither Async Patterns

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "fp-ts-task-either" with this command: npx skills add whatiskadudoing/fp-ts-skills/whatiskadudoing-fp-ts-skills-fp-ts-task-either

fp-ts TaskEither Async Patterns

TaskEither combines the laziness of Task with the error handling of Either, providing a powerful abstraction for async operations that can fail.

Core Concepts

import * as TE from 'fp-ts/TaskEither' import * as T from 'fp-ts/Task' import * as E from 'fp-ts/Either' import { pipe, flow } from 'fp-ts/function'

TaskEither<E, A> is equivalent to () => Promise<Either<E, A>>

  • E = Error type (left)

  • A = Success type (right)

  • Lazy: nothing executes until you call the function

  • Composable: chain operations without try/catch

  1. Converting Promises to TaskEither

Using tryCatch

The primary way to lift Promises into TaskEither:

import * as TE from 'fp-ts/TaskEither'

// Basic tryCatch pattern const fetchUser = (id: string): TE.TaskEither<Error, User> => TE.tryCatch( () => fetch(/api/users/${id}).then(res => res.json()), (reason) => new Error(String(reason)) )

// With typed errors interface ApiError { code: string message: string status: number }

const fetchUserTyped = (id: string): TE.TaskEither<ApiError, User> => TE.tryCatch( () => fetch(/api/users/${id}).then(res => res.json()), (reason): ApiError => ({ code: 'FETCH_ERROR', message: reason instanceof Error ? reason.message : 'Unknown error', status: 500 }) )

From existing Either

// Lift an Either into TaskEither const fromEither: TE.TaskEither<Error, number> = TE.fromEither(E.right(42))

// From a nullable value const fromNullable = TE.fromNullable(new Error('Value was null')) const result = fromNullable(maybeValue) // TaskEither<Error, NonNullable<T>>

// From an Option import * as O from 'fp-ts/Option' const fromOption = TE.fromOption(() => new Error('None value')) const optionResult = fromOption(O.some(42)) // TaskEither<Error, number>

Creating TaskEither values directly

// Success value const success = TE.right<Error, number>(42)

// Error value const failure = TE.left<Error, number>(new Error('Something failed'))

// From a predicate const validatePositive = TE.fromPredicate( (n: number) => n > 0, (n) => new Error(Expected positive, got ${n}) )

  1. Handling Async Errors Functionally

Mapping over errors

// Transform the error type const withMappedError = pipe( fetchUser('123'), TE.mapLeft((error) => ({ type: 'USER_FETCH_ERROR' as const, originalError: error, timestamp: Date.now() })) )

// Bifunctor: map both sides const mapped = pipe( fetchUser('123'), TE.bimap( (error) => new DetailedError(error), // map error (user) => user.profile // map success ) )

Error filtering

// Filter with error on false const validateAge = pipe( fetchUser('123'), TE.filterOrElse( (user) => user.age >= 18, (user) => new Error(User ${user.name} is underage) ) )

  1. Chaining Async Operations

Sequential chaining with chain/flatMap

interface User { id: string; name: string; teamId: string } interface Team { id: string; name: string; orgId: string } interface Org { id: string; name: string }

const fetchUser = (id: string): TE.TaskEither<Error, User> => TE.tryCatch(() => api.getUser(id), toError)

const fetchTeam = (teamId: string): TE.TaskEither<Error, Team> => TE.tryCatch(() => api.getTeam(teamId), toError)

const fetchOrg = (orgId: string): TE.TaskEither<Error, Org> => TE.tryCatch(() => api.getOrg(orgId), toError)

// Chain operations sequentially const getUserOrg = (userId: string): TE.TaskEither<Error, Org> => pipe( fetchUser(userId), TE.chain((user) => fetchTeam(user.teamId)), TE.chain((team) => fetchOrg(team.orgId)) )

// flatMap is an alias for chain const getUserOrgAlt = (userId: string): TE.TaskEither<Error, Org> => pipe( fetchUser(userId), TE.flatMap((user) => fetchTeam(user.teamId)), TE.flatMap((team) => fetchOrg(team.orgId)) )

Chaining with intermediate values

// Use bind to accumulate values const getFullContext = (userId: string) => pipe( TE.Do, TE.bind('user', () => fetchUser(userId)), TE.bind('team', ({ user }) => fetchTeam(user.teamId)), TE.bind('org', ({ team }) => fetchOrg(team.orgId)), TE.map(({ user, team, org }) => ({ userName: user.name, teamName: team.name, orgName: org.name })) )

  1. Parallel vs Sequential Execution

Parallel execution with sequenceArray

import * as A from 'fp-ts/Array'

const userIds = ['1', '2', '3', '4', '5']

// Parallel: all requests start immediately // Fails fast: returns first error encountered const fetchAllUsersParallel = pipe( userIds.map(fetchUser), TE.sequenceArray // TaskEither<Error, readonly User[]> )

// Sequential: one at a time (use when order matters or rate limiting) const fetchAllUsersSequential = pipe( userIds, A.traverse(TE.ApplicativeSeq)(fetchUser) )

Parallel with traverseArray

// More idiomatic: traverse combines map + sequence const fetchAllUsers = (ids: string[]): TE.TaskEither<Error, readonly User[]> => pipe(ids, TE.traverseArray(fetchUser))

// With index const fetchWithIndex = pipe( userIds, TE.traverseArrayWithIndex((index, id) => pipe( fetchUser(id), TE.map(user => ({ ...user, index })) ) ) )

Collecting all errors vs fail fast

import * as These from 'fp-ts/These' import * as TH from 'fp-ts/TaskThese'

// For collecting all errors, consider TaskThese // Or use validation with sequenceT

import { sequenceT } from 'fp-ts/Apply'

// Parallel execution, collects results const parallel = sequenceT(TE.ApplyPar)( fetchUser('1'), fetchTeam('team-1'), fetchOrg('org-1') ) // TaskEither<Error, [User, Team, Org]>

// Sequential execution const sequential = sequenceT(TE.ApplySeq)( fetchUser('1'), fetchTeam('team-1'), fetchOrg('org-1') )

Concurrent with limit

// For controlled concurrency, batch your operations const batchSize = 3

const fetchInBatches = (ids: string[]): TE.TaskEither<Error, User[]> => { const batches = chunk(ids, batchSize)

return pipe( batches, A.traverse(TE.ApplicativeSeq)((batch) => pipe(batch, TE.traverseArray(fetchUser)) ), TE.map(A.flatten) ) }

  1. Error Recovery with orElse

Basic error recovery

// Try primary, fall back to secondary const fetchWithFallback = pipe( fetchFromPrimaryApi(id), TE.orElse((primaryError) => pipe( fetchFromBackupApi(id), TE.mapLeft((backupError) => ({ primary: primaryError, backup: backupError })) ) ) )

// Recover to a default value const fetchWithDefault = pipe( fetchUser(id), TE.orElse(() => TE.right(defaultUser)) )

// orElseW when recovery has different error type const fetchWithTypedFallback = pipe( fetchFromApi(id), // TaskEither<ApiError, User> TE.orElseW((apiError) => // orElseW allows different error type fetchFromCache(id) // TaskEither<CacheError, User> ) ) // TaskEither<CacheError, User>

Retry patterns

const retry = <E, A>( te: TE.TaskEither<E, A>, retries: number, delay: number ): TE.TaskEither<E, A> => pipe( te, TE.orElse((error) => retries > 0 ? pipe( T.delay(delay)(T.of(undefined)), T.chain(() => retry(te, retries - 1, delay * 2)) ) : TE.left(error) ) )

// Usage const fetchWithRetry = retry(fetchUser('123'), 3, 1000)

Conditional recovery

// Only recover from specific errors const recoverFromNotFound = pipe( fetchUser(id), TE.orElse((error) => error.code === 'NOT_FOUND' ? TE.right(createDefaultUser(id)) : TE.left(error) // re-throw other errors ) )

// Alt: try alternatives in order import { alt } from 'fp-ts/TaskEither'

const fetchFromAnywhere = pipe( fetchFromCache(id), TE.alt(() => fetchFromApi(id)), TE.alt(() => fetchFromBackup(id)) )

  1. Pattern Matching Async Results

Using fold/match

// fold executes the TaskEither and handles both cases const handleResult = pipe( fetchUser('123'), TE.fold( (error) => T.of(Error: ${error.message}), (user) => T.of(Welcome, ${user.name}) ) ) // Task<string> - no longer has error channel

// match is an alias for fold const handleWithMatch = pipe( fetchUser('123'), TE.match( (error) => ({ success: false, error }), (user) => ({ success: true, data: user }) ) )

// matchW when handlers return different types const handleWithMatchW = pipe( fetchUser('123'), TE.matchW( (error) => ({ type: 'error' as const, error }), (user) => ({ type: 'success' as const, user }) ) )

Getting the underlying Either

// Execute and get the Either const getEither = async () => { const either = await fetchUser('123')()

if (E.isLeft(either)) { console.error('Failed:', either.left) } else { console.log('User:', either.right) } }

// Using getOrElse for default const getWithDefault = pipe( fetchUser('123'), TE.getOrElse((error) => T.of(defaultUser)) ) // Task<User>

// getOrElseW when default has different type const getOrNull = pipe( fetchUser('123'), TE.getOrElseW(() => T.of(null)) ) // Task<User | null>

  1. Do Notation for Complex Workflows

Building complex operations

interface OrderContext { user: User cart: Cart payment: PaymentMethod shipping: ShippingAddress }

const processOrder = ( userId: string, cartId: string ): TE.TaskEither<OrderError, OrderConfirmation> => pipe( TE.Do, // Bind values sequentially TE.bind('user', () => fetchUser(userId)), TE.bind('cart', () => fetchCart(cartId)),

// Validate intermediate results
TE.filterOrElse(
  ({ cart }) => cart.items.length > 0,
  () => ({ code: 'EMPTY_CART', message: 'Cart is empty' })
),

// Continue building context
TE.bind('payment', ({ user }) => getDefaultPayment(user.id)),
TE.bind('shipping', ({ user }) => getDefaultShipping(user.id)),

// Calculate derived values
TE.bind('total', ({ cart }) => TE.right(calculateTotal(cart))),

// Validate before final operation
TE.filterOrElse(
  ({ payment, total }) => payment.limit >= total,
  ({ total }) => ({ code: 'LIMIT_EXCEEDED', message: `Order total ${total} exceeds limit` })
),

// Final operation
TE.chain(({ user, cart, payment, shipping, total }) =>
  createOrder({ user, cart, payment, shipping, total })
)

)

Parallel fetching within Do

const getOrderDetails = (orderId: string) => pipe( TE.Do, TE.bind('order', () => fetchOrder(orderId)),

// Parallel fetch based on order data
TE.bind('details', ({ order }) =>
  sequenceT(TE.ApplyPar)(
    fetchUser(order.userId),
    fetchProducts(order.productIds),
    fetchShipping(order.shippingId)
  )
),

TE.map(({ order, details: [user, products, shipping] }) => ({
  order,
  user,
  products,
  shipping
}))

)

Using apS for simpler additions

// apS is like bind but doesn't depend on previous values const enrichUser = (userId: string) => pipe( fetchUser(userId), TE.bindTo('user'), // Wrap in { user: ... } TE.apS('config', fetchAppConfig()), // Add independent value TE.apS('features', fetchFeatureFlags()), TE.bind('preferences', ({ user }) => fetchPreferences(user.id)) // Dependent )

  1. Real-World API Call Patterns

Typed API client

interface ApiConfig { baseUrl: string timeout: number }

interface ApiError { code: string message: string status: number details?: unknown }

const createApiClient = (config: ApiConfig) => { const request = <T>( method: string, path: string, body?: unknown ): TE.TaskEither<ApiError, T> => TE.tryCatch( async () => { const response = await fetch(${config.baseUrl}${path}, { method, headers: { 'Content-Type': 'application/json' }, body: body ? JSON.stringify(body) : undefined, signal: AbortSignal.timeout(config.timeout) })

    if (!response.ok) {
      const error = await response.json().catch(() => ({}))
      throw { status: response.status, ...error }
    }

    return response.json()
  },
  (error): ApiError => ({
    code: 'API_ERROR',
    message: error instanceof Error ? error.message : 'Request failed',
    status: (error as any)?.status ?? 500,
    details: error
  })
)

return { get: <T>(path: string) => request<T>('GET', path), post: <T>(path: string, body: unknown) => request<T>('POST', path, body), put: <T>(path: string, body: unknown) => request<T>('PUT', path, body), delete: <T>(path: string) => request<T>('DELETE', path) } }

// Usage const api = createApiClient({ baseUrl: '/api', timeout: 5000 })

const getUser = (id: string) => api.get<User>(/users/${id}) const createUser = (data: CreateUserDto) => api.post<User>('/users', data)

Request with validation

import * as t from 'io-ts' import { PathReporter } from 'io-ts/PathReporter'

const UserCodec = t.type({ id: t.string, name: t.string, email: t.string })

type User = t.TypeOf<typeof UserCodec>

const fetchAndValidate = <A>( codec: t.Type<A>, url: string ): TE.TaskEither<Error, A> => pipe( TE.tryCatch( () => fetch(url).then(r => r.json()), (e) => new Error(Fetch failed: ${e}) ), TE.chainEitherK((data) => pipe( codec.decode(data), E.mapLeft((errors) => new Error(Validation failed: ${PathReporter.report(E.left(errors)).join(', ')}) ) ) ) )

const getValidatedUser = (id: string) => fetchAndValidate(UserCodec, /api/users/${id})

  1. Database Operation Patterns

Repository pattern

interface Repository<E, T, ID> { findById: (id: ID) => TE.TaskEither<E, T> findAll: () => TE.TaskEither<E, readonly T[]> save: (entity: T) => TE.TaskEither<E, T> delete: (id: ID) => TE.TaskEither<E, void> }

interface DbError { code: 'NOT_FOUND' | 'DUPLICATE' | 'CONSTRAINT' | 'CONNECTION' message: string cause?: unknown }

const createUserRepository = (db: Database): Repository<DbError, User, string> => ({ findById: (id) => pipe( TE.tryCatch( () => db.query('SELECT * FROM users WHERE id = ?', [id]), (e): DbError => ({ code: 'CONNECTION', message: String(e), cause: e }) ), TE.chain((rows) => rows.length === 0 ? TE.left({ code: 'NOT_FOUND', message: User ${id} not found }) : TE.right(rows[0] as User) ) ),

findAll: () => TE.tryCatch( () => db.query('SELECT * FROM users'), (e): DbError => ({ code: 'CONNECTION', message: String(e), cause: e }) ),

save: (user) => TE.tryCatch( () => db.query( 'INSERT INTO users (id, name, email) VALUES (?, ?, ?) ON CONFLICT (id) DO UPDATE SET name = ?, email = ?', [user.id, user.name, user.email, user.name, user.email] ).then(() => user), (e): DbError => { if (String(e).includes('UNIQUE constraint')) { return { code: 'DUPLICATE', message: 'Email already exists', cause: e } } return { code: 'CONNECTION', message: String(e), cause: e } } ),

delete: (id) => TE.tryCatch( () => db.query('DELETE FROM users WHERE id = ?', [id]).then(() => undefined), (e): DbError => ({ code: 'CONNECTION', message: String(e), cause: e }) ) })

Transaction handling

interface Transaction { query: (sql: string, params?: unknown[]) => Promise<unknown> commit: () => Promise<void> rollback: () => Promise<void> }

const withTransaction = <E, A>( db: Database, operation: (tx: Transaction) => TE.TaskEither<E, A> ): TE.TaskEither<E | DbError, A> => pipe( TE.tryCatch( () => db.beginTransaction(), (e): DbError => ({ code: 'CONNECTION', message: 'Failed to start transaction', cause: e }) ), TE.chain((tx) => pipe( operation(tx), TE.chainFirst(() => TE.tryCatch( () => tx.commit(), (e): DbError => ({ code: 'CONNECTION', message: 'Commit failed', cause: e }) ) ), TE.orElse((error) => pipe( TE.tryCatch(() => tx.rollback(), () => error), TE.chain(() => TE.left(error)) ) ) ) ) )

// Usage const transferFunds = (fromId: string, toId: string, amount: number) => withTransaction(db, (tx) => pipe( TE.Do, TE.bind('from', () => getAccount(tx, fromId)), TE.bind('to', () => getAccount(tx, toId)), TE.filterOrElse( ({ from }) => from.balance >= amount, () => ({ code: 'INSUFFICIENT_FUNDS', message: 'Not enough balance' }) ), TE.chain(({ from, to }) => sequenceT(TE.ApplySeq)( updateBalance(tx, fromId, from.balance - amount), updateBalance(tx, toId, to.balance + amount) ) ), TE.map(() => ({ success: true, amount })) ) )

  1. Task vs TaskEither: When to Use Which

Use Task when:

import * as T from 'fp-ts/Task'

// 1. Operation cannot fail const delay = (ms: number): T.Task<void> => () => new Promise(resolve => setTimeout(resolve, ms))

// 2. Errors are handled elsewhere const logMessage = (msg: string): T.Task<void> => () => console.log(msg) as unknown as Promise<void>

// 3. You want to ignore errors const fetchOrDefault = (url: string, defaultValue: Data): T.Task<Data> => pipe( TE.tryCatch(() => fetch(url).then(r => r.json()), E.toError), TE.getOrElse(() => T.of(defaultValue)) )

// 4. Fire and forget const trackAnalytics = (event: Event): T.Task<void> => () => analytics.track(event).catch(() => {}) // Errors swallowed

Use TaskEither when:

// 1. Operation can fail and you need to handle the error const fetchUser = (id: string): TE.TaskEither<Error, User> => TE.tryCatch(() => api.getUser(id), E.toError)

// 2. You need typed errors for different failure modes type AuthError = | { type: 'INVALID_CREDENTIALS' } | { type: 'EXPIRED_TOKEN' } | { type: 'NETWORK_ERROR'; cause: Error }

const authenticate = (token: string): TE.TaskEither<AuthError, User> => { /* ... */ }

// 3. Error recovery is part of business logic const getConfig = (): TE.TaskEither<ConfigError, Config> => pipe( fetchRemoteConfig(), TE.orElse(() => loadLocalConfig()), TE.orElse(() => TE.right(defaultConfig)) )

// 4. Composing multiple fallible operations const processOrder = (orderId: string): TE.TaskEither<OrderError, Receipt> => pipe( validateOrder(orderId), TE.chain(chargePayment), TE.chain(fulfillOrder), TE.chain(sendConfirmation) )

Converting between them

// Task to TaskEither (infallible to fallible) const taskToTE = <A>(task: T.Task<A>): TE.TaskEither<never, A> => pipe(task, T.map(E.right))

// TaskEither to Task (handle/ignore error) const teToTask = <E, A>(te: TE.TaskEither<E, A>, defaultValue: A): T.Task<A> => TE.getOrElse(() => T.of(defaultValue))(te)

// TaskEither to Task (throw on error - escape hatch) const teToTaskThrow = <E, A>(te: TE.TaskEither<E, A>): T.Task<A> => pipe( te, TE.getOrElse((e) => () => Promise.reject(e)) )

Quick Reference

Operation Function Description

Create success TE.right(value)

Wrap value in Right

Create failure TE.left(error)

Wrap error in Left

From Promise TE.tryCatch(promise, onError)

Convert Promise to TE

Transform value TE.map(f)

Apply f to success value

Transform error TE.mapLeft(f)

Apply f to error value

Chain operations TE.chain(f) / TE.flatMap(f)

Sequence dependent operations

Recover from error TE.orElse(f)

Try alternative on error

Handle both cases TE.fold(onError, onSuccess)

Pattern match result

Parallel array TE.traverseArray(f)

Map + sequence in parallel

Sequential array A.traverse(TE.ApplicativeSeq)(f)

Map + sequence in order

Filter with error TE.filterOrElse(pred, onFalse)

Validate with error

Get or default TE.getOrElse(onError)

Extract value with fallback

Common Patterns Summary

// 1. Fetch with error handling const fetch = TE.tryCatch(() => api.get(url), toError)

// 2. Chain dependent calls pipe(getA(), TE.chain(a => getB(a.id)), TE.chain(b => getC(b.id)))

// 3. Parallel independent calls sequenceT(TE.ApplyPar)(getA(), getB(), getC())

// 4. Build context with Do pipe(TE.Do, TE.bind('a', () => getA()), TE.bind('b', ({a}) => getB(a)))

// 5. Recover from errors pipe(primary(), TE.orElse(() => fallback()))

// 6. Execute and handle result pipe(operation(), TE.fold(handleError, handleSuccess))()

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

fp-ts-backend

No summary provided by upstream source.

Repository SourceNeeds Review
General

fp-immutable

No summary provided by upstream source.

Repository SourceNeeds Review
General

fp-refactor

No summary provided by upstream source.

Repository SourceNeeds Review
General

practical error handling with fp-ts

No summary provided by upstream source.

Repository SourceNeeds Review