Handle errors as values using fp-ts Either and TaskEither for cleaner, more predictable TypeScript code. Use when implementing error handling patterns with fp-ts.
Instalar com Codex ou Claude Copie este prompt, cole no Codex, Claude ou outro assistente e deixe que ele revise a página da skill e instale para você.
Um comando direto ignora o prompt de revisão. Verifique a origem antes de executá-lo.
Instruções da origem · Visualização somente leitura
name
fp-ts-errors
description
Handle errors as values using fp-ts Either and TaskEither for cleaner, more predictable TypeScript code. Use when implementing error handling patterns with fp-ts.
This skill teaches you how to handle errors without try/catch spaghetti. No academic jargon - just practical patterns for real problems.
When to Use This Skill
When you want type-safe error handling in TypeScript
When replacing try/catch with Either and TaskEither patterns
When building APIs or services that need explicit error types
When accumulating multiple validation errors
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.
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!
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
}
After: Clean Chain with Either
import * as E from'fp-ts/Either'import { pipe } from'fp-ts/function'// Each function returns Either<Error, T>const getUser = (id: string): E.Either<string, User> => { ... }
const getProduct = (id: string): E.Either<string, Product> => { ... }
const createOrder = (user: User, product: Product): E.Either<string, Order> => { ... }
// Chain them together - first error stops the chainconst processUserOrder = (userId: string, productId: string): E.Either<string, Order> =>
pipe(
getUser(userId),
E.filterOrElse(
user => user.isActive,
() =>'User not active'
),
E.chain(user =>pipe(
getProduct(productId),
E.filterOrElse(
product => product.stock >= 1,
() =>'Out of stock'
),
E.chain(product =>createOrder(user, product))
)
)
)
// Or use Do notation for cleaner access to intermediate valuesconst processUserOrder = (userId: string, productId: string): E.Either<string, Order> =>
pipe(
E.Do,
E.bind('user', () =>getUser(userId)),
E.filterOrElse(
({ user }) => user.isActive,
() =>'User not active'
),
E.bind('product', () =>getProduct(productId)),
E.filterOrElse(
({ product }) => product.stock >= 1,
() =>'Out of stock'
),
E.chain(({ user, product }) =>createOrder(user, product))
)
import * as T from'fp-ts/Task'import * asTEfrom'fp-ts/TaskEither'import { pipe } from'fp-ts/function'const retry = <E, A>(
task: TE.TaskEither<E, A>,
attempts: number,
delayMs: number
): TE.TaskEither<E, A> =>
pipe(
task,
TE.orElse((error) =>
attempts > 1
? pipe(
T.delay(delayMs)(T.of(undefined)),
T.chain(() =>retry(task, attempts - 1, delayMs * 2))
)
: TE.left(error)
)
)
// Retry up to 3 times with exponential backoffconst fetchWithRetry = retry(fetchUser('123'), 3, 1000)
Fallback to Alternative
// Try cache first, fall back to APIconstgetUserData = (id: string) =>
pipe(
fetchFromCache(id),
TE.orElse(() =>fetchFromApi(id)),
TE.orElse(() =>TE.right(defaultUser)) // Last resort default
)
6. Converting Between Patterns
Real codebases have throwing functions, nullable values, and promises. Here's how to work with them.
From Nullable to Either
import * as E from'fp-ts/Either'import * as O from'fp-ts/Option'// Direct conversionconst user = users.find(u => u.id === id) // User | undefinedconst result = E.fromNullable('User not found')(user)
// From OptionconstmaybeUser: O.Option<User> = O.fromNullable(user)
const eitherUser = pipe(
maybeUser,
E.fromOption(() =>'User not found')
)
From Throwing Function to Either
// Wrap at the boundaryconst safeParse = <T>(schema: ZodSchema<T>) => (data: unknown): E.Either<ZodError, T> =>
E.tryCatch(
() => schema.parse(data),
(e) => e asZodError
)
// Use throughout your codeconst parseUser = safeParse(UserSchema)
const result = parseUser(rawData) // Either<ZodError, User>