Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
A direct command skips the review prompt. Inspect the source before running it.
A function to convert the thrown value into your error type
TE.tryCatch(
() => somePromise, // The async work(thrown) =>toError(thrown) // Convert failures to your error type
)
Creating Success and Failure Values
// Wrap a value as successconst success = TE.right<Error, number>(42)
// Wrap a value as failureconst failure = TE.left<Error, number>(newError('Nope'))
// From a nullable value (null/undefined becomes error)const fromNullable = TE.fromNullable(newError('Value was null'))
const result = fromNullable(maybeUser) // TaskEither<Error, User>// From a conditionconst mustBePositive = TE.fromPredicate(
(n: number) => n > 0,
(n) =>newError(`Expected positive, got ${n}`)
)
2. Chaining Async Operations
The Problem: Callback Hell / Nested Awaits
// BEFORE: Deeply nested, hard to followasyncfunctionprocessOrder(orderId: string) {
try {
const order = awaitfetchOrder(orderId)
if (!order) thrownewError('Order not found')
try {
const user = awaitfetchUser(order.userId)
if (!user) thrownewError('User not found')
try {
const inventory = awaitcheckInventory(order.items)
if (!inventory.available) thrownewError('Out of stock')
try {
const payment = awaitchargePayment(user, order.total)
if (!payment.success) thrownewError('Payment failed')
try {
const shipment = awaitcreateShipment(order, user)
return { order, shipment, payment }
} catch (e) {
// Refund payment? Log? What's the state now?awaitrefundPayment(payment.id)
throw e
}
} catch (e) {
throw e
}
} catch (e) {
throw e
}
} catch (e) {
throw e
}
} catch (e) {
console.error('Order processing failed', e)
throw e
}
}
When each operation depends on the previous result
When you need to respect rate limits
When order matters
Parallel (all at once):
When operations are independent
When you want speed
When fetching multiple resources by ID
Sequential Chaining
// Operations depend on each other - must be sequentialconstgetUserWithOrg = (userId: string) =>
pipe(
fetchUser(userId), // First: get userTE.chain(user =>fetchTeam(user.teamId)), // Then: get their teamTE.chain(team =>fetchOrganization(team.orgId)) // Finally: get org
)
// Fetch multiple users in parallelconst userIds = ['1', '2', '3', '4', '5']
// TE.traverseArray runs all fetches in parallelconst fetchAllUsers = pipe(
userIds,
TE.traverseArray(fetchUser)
) // TaskEither<Error, readonly User[]>// Note: Fails fast - if ANY request fails, the whole thing fails// All errors after the first are lost
Parallel with Batch Control
When you need to limit concurrent requests:
const chunk = <T>(arr: T[], size: number): T[][] => {
constchunks: T[][] = []
for (let i = 0; i < arr.length; i += size) {
chunks.push(arr.slice(i, i + size))
}
return chunks
}
// Process in batches of 5 concurrent requestsconstfetchUsersWithLimit = (userIds: string[]) => {
const batches = chunk(userIds, 5)
returnpipe(
batches,
// Process batches sequentiallyTE.traverseArray(batch =>// But within each batch, run in parallelpipe(batch, TE.traverseArray(fetchUser))
),
TE.map(results => results.flat())
)
}
Sequential When Parallel Looks Tempting
// WRONG: This looks parallel but order might matter for DB operationsconstcreateUserAndProfile = (userData: UserData) =>
sequenceT(TE.ApplyPar)(
createUser(userData), // Creates user with IDcreateProfile(userData.profile) // Needs user ID - race condition!
)
// RIGHT: Sequential when there's a dependencyconstcreateUserAndProfile = (userData: UserData) =>
pipe(
createUser(userData),
TE.chain(user =>pipe(
createProfile(user.id, userData.profile),
TE.map(profile => ({ user, profile }))
)
)
)
// fold: Handle both success and failure, returns a Task (no more error channel)const displayResult = pipe(
fetchUser(userId),
TE.fold(
(error) => T.of(`Error: ${error.message}`),
(user) => T.of(`Welcome, ${user.name}!`)
)
) // Task<string>// Execute and get the stringconst message = awaitdisplayResult()
Getting the Raw Either
// Sometimes you need to work with the Either directlyconst result = awaitfetchUser(userId)() // Either<Error, User>if (E.isLeft(result)) {
console.error('Failed:', result.left)
} else {
console.log('User:', result.right)
}
// Transform success valueTE.map(user => user.name)
// Transform errorTE.mapLeft(error => ({ ...error, timestamp: Date.now() }))
// Transform both at onceTE.bimap(
error =>enhanceError(error),
user => user.profile
)
Filtering
// Fail if condition not metpipe(
fetchUser(userId),
TE.filterOrElse(
user => user.isActive,
user =>newError(`User ${user.id} is not active`)
)
)
Side Effects Without Changing Value
// Log on success, keep the value unchangedpipe(
fetchUser(userId),
TE.tap(user =>TE.fromIO(() =>console.log(`Fetched user: ${user.id}`)))
)
// Log on error, keep the error unchangedpipe(
fetchUser(userId),
TE.tapError(error =>TE.fromIO(() =>console.error(`Failed: ${error.message}`)))
)
// chainFirst is like tap but for operations that return TaskEitherpipe(
createUser(userData),
TE.chainFirst(user =>sendWelcomeEmail(user.email))
) // Returns the created user, not the email result
Converting From Other Types
// From Eitherconst fromEither = TE.fromEither(E.right(42))
// From Optionimport * as O from'fp-ts/Option'const fromOption = TE.fromOption(() =>newError('Value was None'))
const result = fromOption(O.some(42))
// From booleanconst fromBoolean = TE.fromPredicate(
(x: number) => x > 0,
() =>newError('Must be positive')
)