| name | fp-errors |
| description | Stop throwing everywhere - handle errors as values using Either and TaskEither for cleaner, more predictable code |
| category | Business & Marketing |
| source | antigravity |
| tags | ["typescript","api","ai","prisma","rag"] |
| url | https://github.com/sickn33/antigravity-awesome-skills/tree/main/skills/fp-errors |
Practical Error Handling with fp-ts
This skill teaches you how to handle errors without try/catch spaghetti. No academic jargon - just practical patterns for real problems.
The core idea: Errors are just data. Instead of throwing them into the void and hoping someone catches them, return them as values that TypeScript can track.
When to Use
- You need to replace exception-heavy code with
Either or TaskEither.
- The task involves validation, domain errors, or clearer error contracts in TypeScript.
- You want pragmatic fp-ts error-handling guidance for real application code.
1. Stop Throwing Everywhere
The Problem with Exceptions
Exceptions are invisible in your types. They break the contract between functions.
function getUser(id: string): User
function getUser(id: string): User {
if (!id) throw new Error('ID required')
const user = db.find(id)
if (!user) throw new Error('User not found')
return user
}
const user = getUser(id)
You end up with code like this:
function processOrder(orderId: string) {
let order
try {
order = getOrder(orderId)
} catch (e) {
console.error('Failed to get order')
return null
}
let user
try {
user = getUser(order.userId)
} catch (e) {
console.error('Failed to get user')
return null
}
let payment
try {
payment = chargeCard(user.cardId, order.total)
} catch (e) {
console.error('Payment failed')
return null
}
return { order, user, payment }
}
The Solution: Return Errors as Values
import * as E from 'fp-ts/Either'
import { pipe } from 'fp-ts/function'
function getUser(id: string): E.Either<string, User> {
if (!id) return E.left('ID required')
const user = db.find(id)
if (!user) return E.left('User not found')
return E.right(user)
}
const result = getUser(id)
2. The Result Pattern (Either)
Either<E, A> is simple: it holds either an error (E) or a value (A).
Left = error case
Right = success case (think "right" as in "correct")
import * as E from 'fp-ts/Either'
const success = E.right(42)
const failure = E.left('Oops')
if (E.isRight(result)) {
console.log(result.right)
} else {
console.log(result.left)
}
const message = pipe(
result,
E.fold(
(error) => `Failed: ${error}`,
(value) => `Got: ${value}`
)
)
Converting Throwing Code to Either
const parseJSON = (json: string): E.Either<Error, unknown> =>
E.tryCatch(
() => JSON.parse(json),
(e) => (e instanceof Error ? e : new Error(String(e)))
)
parseJSON('{"valid": true}')
parseJSON('not json')
const safeParseJSON = E.tryCatchK(
JSON.parse,
(e) => (e instanceof Error ? e : new Error(String(e)))
)
Common Either Operations
import * as E from 'fp-ts/Either'
import { pipe } from 'fp-ts/function'
const doubled = pipe(
E.right(21),
E.map(n => n * 2)
)
const betterError = pipe(
E.left('bad'),
E.mapLeft(e => `Error: ${e}`)
)
const value = pipe(
E.left('failed'),
E.getOrElse(() => 0)
)
const fromNullable = E.fromNullable('not found')
fromNullable(user)
3. Chaining Operations That Might Fail
The real power comes from chaining. Each step can fail, but you write it as a clean pipeline.
Before: Nested Try/Catch Hell
function processUserOrder(userId: string, productId: string): Result | null {
let user
try {
user = getUser(userId)
} catch (e) {
logError('User fetch failed', e)
return null
}
if (!user.isActive) {
logError('User not active')
return null
}
let product
try {
product = getProduct(productId)
} catch (e) {
logError('Product fetch failed', e)
return null
}
if (product.stock < 1) {
logError('Out of stock')
return null
}
let order
try {
order = createOrder(user, product)
} catch (e) {
logError('Order creation failed', e)
return null
}
return order
}
After: Clean Chain with Either
import * as E from 'fp-ts/Either