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.
// What this function signature promises:functiongetUser(id: string): User// What it actually does:functiongetUser(id: string): User {
if (!id) thrownewError('ID required')
const user = db.find(id)
if (!user) thrownewError('User not found')
return user
}
// The caller has no idea this can failconst user = getUser(id) // Might explode!
You end up with code like this:
// MESSY: try/catch everywherefunctionprocessOrder(orderId: string) {
let order
try {
order = getOrder(orderId)
} catch (e) {
console.error('Failed to get order')
returnnull
}
let user
try {
user = getUser(order.userId)
} catch (e) {
console.error('Failed to get user')
returnnull
}
let payment
try {
payment = chargeCard(user.cardId, order.total)
} catch (e) {
console.error('Payment failed')
returnnull
}
return { order, user, payment }
}
The Solution: Return Errors as Values
import * as E from'fp-ts/Either'import { pipe } from'fp-ts/function'// Now TypeScript KNOWS this can failfunctiongetUser(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)
}
// The caller is forced to handle both casesconst result = getUser(id)
// result is Either<string, User> - error OR success, never both
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'// Creating valuesconst success = E.right(42) // Right(42)const failure = E.left('Oops') // Left('Oops')// Checking what you haveif (E.isRight(result)) {
console.log(result.right) // The success value
} else {
console.log(result.left) // The error
}
// Better: pattern match with foldconst message = pipe(
result,
E.fold(
(error) =>`Failed: ${error}`,
(value) =>`Got: ${value}`
)
)
Converting Throwing Code to Either
// Wrap any throwing function with tryCatchconst parseJSON = (json: string): E.Either<Error, unknown> =>
E.tryCatch(
() =>JSON.parse(json),
(e) => (e instanceofError ? e : newError(String(e)))
)
parseJSON('{"valid": true}') // Right({ valid: true })parseJSON('not json') // Left(SyntaxError: ...)// For functions you'll reuse, use tryCatchKconst safeParseJSON = E.tryCatchK(
JSON.parse,
(e) => (e instanceofError ? e : newError(String(e)))
)
Common Either Operations
import * as E from'fp-ts/Either'import { pipe } from'fp-ts/function'// Transform the success valueconst doubled = pipe(
E.right(21),
E.map(n => n * 2)
) // Right(42)// Transform the errorconst betterError = pipe(
E.left('bad'),
E.mapLeft(e =>`Error: ${e}`)
) // Left('Error: bad')// Provide a default for errorsconst value = pipe(
E.left('failed'),
E.getOrElse(() =>0)
) // 0// Convert nullable to Eitherconst fromNullable = E.fromNullable('not found')
fromNullable(user) // Right(user) if exists, Left('not found') if null/undefined
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
// MESSY: Each step can fail, nested try/catch everywherefunctionprocessUserOrder(userId: string, productId: string): Result | null {
let user
try {
user = getUser(userId)
} catch (e) {
logError('User fetch failed', e)
returnnull
}
if (!user.isActive) {
logError('User not active')
returnnull
}
let product
try {
product = getProduct(productId)
} catch (e) {
logError('Product fetch failed', e)
returnnull
}
if (product.stock < 1) {
logError('Out of stock')
returnnull
}
let order
try {
order = createOrder(user, product)
} catch (e) {
logError('Order creation failed', e)
returnnull
}
return order
}