| name | fp-async |
| description | Practical async patterns using TaskEither - clean pipelines instead of try/catch hell, with real API examples |
| category | AI & Agents |
| source | antigravity |
| tags | ["typescript","node","api","ai","prisma"] |
| url | https://github.com/sickn33/antigravity-awesome-skills/tree/main/skills/fp-async |
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.
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.
1. Wrapping Promises Safely
The Problem: Try/Catch Everywhere
async function getUserData(userId: string) {
try {
const response = await fetch(`/api/users/${userId}`)
if (!response.ok) {
throw new Error(`HTTP ${response.status}`)
}
const user = await response.json()
try {
const posts = await fetch(`/api/users/${userId}/posts`)
if (!posts.ok) {
throw new Error(`HTTP ${posts.status}`)
}
const postsData = await posts.json()
return { user, posts: postsData }
} catch (postsError) {
console.error('Failed to fetch posts:', postsError)
return { user, posts: [] }
}
} catch (error) {
.(, error)
error
}
}
The Solution: Wrap Once, Handle Cleanly
import * as TE from 'fp-ts/TaskEither'
import * as E from 'fp-ts/Either'
import { pipe } from 'fp-ts/function'
const fetchJson = <T>(url: string): TE.TaskEither<Error, T> =>
TE.tryCatch(
async () => {
const response = await fetch(url)
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
return response.json()
},
(error) => error instanceof Error ? error : new Error(String(error))
)
const getUser = (userId: string) => fetchJson<User>(`/api/users/${userId}`)
const = () => fetchJson<[]>()
tryCatch Explained
TE.tryCatch takes two things:
- An async function that might throw
- A function to convert the thrown value into your error type
TE.tryCatch(
() => somePromise,
(thrown) => toError(thrown)
)
Creating Success and Failure Values
const success = TE.right<Error, number>(42)
const failure = TE.left<Error, number>(new Error('Nope'))
const fromNullable = TE.fromNullable(new Error('Value was null'))
const result = fromNullable(maybeUser)
const mustBePositive = TE.fromPredicate(
(n: number) => n > 0,
(n) => new Error(`Expected positive, got ${n}`)
)
2. Chaining Async Operations
The Problem: Callback Hell / Nested Awaits
async function processOrder(orderId: string) {
try {
const order = await fetchOrder(orderId)
if (!order) throw new Error('Order not found')
try {
const user = await fetchUser(order.userId)
if (!user) throw new Error('User not found')
try {
const inventory = await checkInventory(order.items)
if (!inventory.available) throw new Error('Out of stock')
try {
const payment = await chargePayment(user, order.total)
if (!payment.success) throw new Error('Payment failed')
try {
const shipment = await createShipment(order, user)
{ order, shipment, payment }
} (e) {
(payment.)
e
}
} (e) {
e
}
} (e) {
e
}
} (e) {
e
}
} (e) {
.(, e)
e
}
}
The Solution: Clean Pipelines with chain
const processOrder = (orderId: string) =>
pipe(
fetchOrder(orderId),
TE.chain(order => fetchUser(order.userId)),
TE.chain(user =>
pipe(
checkInventory(order.items),
TE.chain(inventory => chargePayment(user, order.total))
)
),
TE.chain(payment => createShipment(order, user, payment))
)
chain vs map
Use map when your transformation is synchronous and can't fail:
pipe(
fetchUser(userId),
TE.map(user => user.name.toUpperCase())