Installer avec Codex ou Claude Copiez ce prompt, collez-le dans Codex, Claude ou un autre assistant, puis laissez-le vérifier la page du skill et l'installer pour vous.
Une commande directe contourne le prompt de vérification. Examinez la source avant de l'exécuter.
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
}
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>