fp-ts-errors
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChinesePractical Error Handling with fp-ts
基于fp-ts的实用错误处理
This skill teaches you how to handle errors without try/catch spaghetti. No academic jargon - just practical patterns for real problems.
本技能将教你如何摆脱混乱的try/catch来处理错误。没有晦涩的学术术语,只有针对实际问题的实用模式。
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.
- 当你需要在TypeScript中实现类型安全的错误处理时
- 当你想用Either和TaskEither模式替代try/catch时
- 当你构建需要显式错误类型的API或服务时
- 当你需要收集多个验证错误时
核心思想:错误只是数据。不要将错误抛出后寄希望于有人捕获,而是将它们作为TypeScript可以追踪的值返回。
1. Stop Throwing Everywhere
1. 停止到处抛出错误
The Problem with Exceptions
异常处理的问题
Exceptions are invisible in your types. They break the contract between functions.
typescript
// What this function signature promises:
function getUser(id: string): User
// What it actually does:
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
}
// The caller has no idea this can fail
const user = getUser(id) // Might explode!You end up with code like this:
typescript
// MESSY: try/catch everywhere
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 }
}异常在类型中是不可见的,它们会破坏函数之间的契约。
typescript
// 函数签名承诺的内容:
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) // 可能会崩溃!你最终会写出这样的代码:
typescript
// 混乱不堪: 到处都是try/catch
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
解决方案: 将错误作为值返回
typescript
import * as E from 'fp-ts/Either'
import { pipe } from 'fp-ts/function'
// Now TypeScript KNOWS this can fail
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)
}
// The caller is forced to handle both cases
const result = getUser(id)
// result is Either<string, User> - error OR success, never bothtypescript
import * as E from 'fp-ts/Either'
import { pipe } from 'fp-ts/function'
// 现在TypeScript明确知道这个函数可能失败
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)
// result的类型是Either<string, User> - 要么是错误,要么是成功,不会同时存在2. The Result Pattern (Either)
2. 结果模式(Either)
Either<E, A>EA- = error case
Left - = success case (think "right" as in "correct")
Right
typescript
import * as E from 'fp-ts/Either'
// Creating values
const success = E.right(42) // Right(42)
const failure = E.left('Oops') // Left('Oops')
// Checking what you have
if (E.isRight(result)) {
console.log(result.right) // The success value
} else {
console.log(result.left) // The error
}
// Better: pattern match with fold
const message = pipe(
result,
E.fold(
(error) => `Failed: ${error}`,
(value) => `Got: ${value}`
)
)Either<E, A>EA- = 错误情况
Left - = 成功情况(可以把"right"理解为"正确")
Right
typescript
import * as E from 'fp-ts/Either'
// 创建值
const success = E.right(42) // Right(42)
const failure = E.left('Oops') // Left('Oops')
// 判断结果类型
if (E.isRight(result)) {
console.log(result.right) // 成功的值
} else {
console.log(result.left) // 错误信息
}
// 更好的方式: 使用fold进行模式匹配
const message = pipe(
result,
E.fold(
(error) => `失败: ${error}`,
(value) => `获取到: ${value}`
)
)Converting Throwing Code to Either
将抛出错误的代码转换为Either
typescript
// Wrap any throwing function with tryCatch
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}') // Right({ valid: true })
parseJSON('not json') // Left(SyntaxError: ...)
// For functions you'll reuse, use tryCatchK
const safeParseJSON = E.tryCatchK(
JSON.parse,
(e) => (e instanceof Error ? e : new Error(String(e)))
)typescript
// 用tryCatch包装任何抛出错误的函数
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}') // Right({ valid: true })
parseJSON('not json') // Left(SyntaxError: ...)
// 对于需要重复使用的函数,使用tryCatchK
const safeParseJSON = E.tryCatchK(
JSON.parse,
(e) => (e instanceof Error ? e : new Error(String(e)))
)Common Either Operations
常见的Either操作
typescript
import * as E from 'fp-ts/Either'
import { pipe } from 'fp-ts/function'
// Transform the success value
const doubled = pipe(
E.right(21),
E.map(n => n * 2)
) // Right(42)
// Transform the error
const betterError = pipe(
E.left('bad'),
E.mapLeft(e => `Error: ${e}`)
) // Left('Error: bad')
// Provide a default for errors
const value = pipe(
E.left('failed'),
E.getOrElse(() => 0)
) // 0
// Convert nullable to Either
const fromNullable = E.fromNullable('not found')
fromNullable(user) // Right(user) if exists, Left('not found') if null/undefinedtypescript
import * as E from 'fp-ts/Either'
import { pipe } from 'fp-ts/function'
// 转换成功的值
const doubled = pipe(
E.right(21),
E.map(n => n * 2)
) // Right(42)
// 转换错误信息
const betterError = pipe(
E.left('bad'),
E.mapLeft(e => `错误: ${e}`)
) // Left('Error: bad')
// 为错误提供默认值
const value = pipe(
E.left('failed'),
E.getOrElse(() => 0)
) // 0
// 将可空值转换为Either
const fromNullable = E.fromNullable('未找到')
fromNullable(user) // 如果user存在则返回Right(user),如果为null/undefined则返回Left('未找到')3. Chaining Operations That Might Fail
3. 链式调用可能失败的操作
The real power comes from chaining. Each step can fail, but you write it as a clean pipeline.
真正的强大之处在于链式调用。每个步骤都可能失败,但你可以写出清晰的流水线代码。
Before: Nested Try/Catch Hell
之前: 嵌套的try/catch地狱
typescript
// MESSY: Each step can fail, nested try/catch everywhere
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
}typescript
// 混乱不堪: 每个步骤都可能失败,到处都是嵌套的try/catch
function processUserOrder(userId: string, productId: string): Result | null {
let user
try {
user = getUser(userId)
} catch (e) {
logError('用户获取失败', e)
return null
}
if (!user.isActive) {
logError('用户未激活')
return null
}
let product
try {
product = getProduct(productId)
} catch (e) {
logError('商品获取失败', e)
return null
}
if (product.stock < 1) {
logError('库存不足')
return null
}
let order
try {
order = createOrder(user, product)
} catch (e) {
logError('订单创建失败', e)
return null
}
return order
}After: Clean Chain with Either
之后: 使用Either实现清晰的链式调用
typescript
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 chain
const 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 values
const 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))
)typescript
import * as E from 'fp-ts/Either'
import { pipe } from 'fp-ts/function'
// 每个函数都返回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> => { ... }
// 链式调用 - 第一个错误会终止整个链式流程
const processUserOrder = (userId: string, productId: string): E.Either<string, Order> =>
pipe(
getUser(userId),
E.filterOrElse(
user => user.isActive,
() => '用户未激活'
),
E.chain(user =>
pipe(
getProduct(productId),
E.filterOrElse(
product => product.stock >= 1,
() => '库存不足'
),
E.chain(product => createOrder(user, product))
)
)
)
// 或者使用Do符号更清晰地访问中间值
const processUserOrder = (userId: string, productId: string): E.Either<string, Order> =>
pipe(
E.Do,
E.bind('user', () => getUser(userId)),
E.filterOrElse(
({ user }) => user.isActive,
() => '用户未激活'
),
E.bind('product', () => getProduct(productId)),
E.filterOrElse(
({ product }) => product.stock >= 1,
() => '库存不足'
),
E.chain(({ user, product }) => createOrder(user, product))
)Different Error Types? Use chainW
不同的错误类型? 使用chainW
typescript
type ValidationError = { type: 'validation'; message: string }
type DbError = { type: 'db'; message: string }
const validateInput = (id: string): E.Either<ValidationError, string> => { ... }
const fetchFromDb = (id: string): E.Either<DbError, User> => { ... }
// chainW (W = "wider") automatically unions the error types
const process = (id: string): E.Either<ValidationError | DbError, User> =>
pipe(
validateInput(id),
E.chainW(validId => fetchFromDb(validId))
)typescript
type ValidationError = { type: 'validation'; message: string }
type DbError = { type: 'db'; message: string }
const validateInput = (id: string): E.Either<ValidationError, string> => { ... }
const fetchFromDb = (id: string): E.Either<DbError, User> => { ... }
// chainW (W = "更宽泛") 会自动合并错误类型
const process = (id: string): E.Either<ValidationError | DbError, User> =>
pipe(
validateInput(id),
E.chainW(validId => fetchFromDb(validId))
)4. Collecting Multiple Errors
4. 收集多个错误
Sometimes you want ALL errors, not just the first one. Form validation is the classic example.
有时你需要获取所有错误,而不仅仅是第一个。表单验证就是典型的例子。
Before: Collecting Errors Manually
之前: 手动收集错误
typescript
// MESSY: Manual error accumulation
function validateForm(form: FormData): { valid: boolean; errors: string[] } {
const errors: string[] = []
if (!form.email) {
errors.push('Email required')
} else if (!form.email.includes('@')) {
errors.push('Invalid email')
}
if (!form.password) {
errors.push('Password required')
} else if (form.password.length < 8) {
errors.push('Password too short')
}
if (!form.age) {
errors.push('Age required')
} else if (form.age < 18) {
errors.push('Must be 18+')
}
return { valid: errors.length === 0, errors }
}typescript
// 混乱不堪: 手动收集错误
function validateForm(form: FormData): { valid: boolean; errors: string[] } {
const errors: string[] = []
if (!form.email) {
errors.push('邮箱必填')
} else if (!form.email.includes('@')) {
errors.push('无效邮箱')
}
if (!form.password) {
errors.push('密码必填')
} else if (form.password.length < 8) {
errors.push('密码过短')
}
if (!form.age) {
errors.push('年龄必填')
} else if (form.age < 18) {
errors.push('必须年满18岁')
}
return { valid: errors.length === 0, errors }
}After: Validation with Error Accumulation
之后: 带错误收集的验证
typescript
import * as E from 'fp-ts/Either'
import * as NEA from 'fp-ts/NonEmptyArray'
import { sequenceS } from 'fp-ts/Apply'
import { pipe } from 'fp-ts/function'
// Errors as a NonEmptyArray (always at least one)
type Errors = NEA.NonEmptyArray<string>
// Create the applicative that accumulates errors
const validation = E.getApplicativeValidation(NEA.getSemigroup<string>())
// Validators that return Either<Errors, T>
const validateEmail = (email: string): E.Either<Errors, string> =>
!email ? E.left(NEA.of('Email required'))
: !email.includes('@') ? E.left(NEA.of('Invalid email'))
: E.right(email)
const validatePassword = (password: string): E.Either<Errors, string> =>
!password ? E.left(NEA.of('Password required'))
: password.length < 8 ? E.left(NEA.of('Password too short'))
: E.right(password)
const validateAge = (age: number | undefined): E.Either<Errors, number> =>
age === undefined ? E.left(NEA.of('Age required'))
: age < 18 ? E.left(NEA.of('Must be 18+'))
: E.right(age)
// Combine all validations - collects ALL errors
const validateForm = (form: FormData) =>
sequenceS(validation)({
email: validateEmail(form.email),
password: validatePassword(form.password),
age: validateAge(form.age)
})
// Usage
validateForm({ email: '', password: '123', age: 15 })
// Left(['Email required', 'Password too short', 'Must be 18+'])
validateForm({ email: 'a@b.com', password: 'longpassword', age: 25 })
// Right({ email: 'a@b.com', password: 'longpassword', age: 25 })typescript
import * as E from 'fp-ts/Either'
import * as NEA from 'fp-ts/NonEmptyArray'
import { sequenceS } from 'fp-ts/Apply'
import { pipe } from 'fp-ts/function'
// 错误类型为NonEmptyArray(至少包含一个错误)
type Errors = NEA.NonEmptyArray<string>
// 创建用于收集错误的applicative
const validation = E.getApplicativeValidation(NEA.getSemigroup<string>())
// 返回Either<Errors, T>的验证器
const validateEmail = (email: string): E.Either<Errors, string> =>
!email ? E.left(NEA.of('邮箱必填'))
: !email.includes('@') ? E.left(NEA.of('无效邮箱'))
: E.right(email)
const validatePassword = (password: string): E.Either<Errors, string> =>
!password ? E.left(NEA.of('密码必填'))
: password.length < 8 ? E.left(NEA.of('密码过短'))
: E.right(password)
const validateAge = (age: number | undefined): E.Either<Errors, number> =>
age === undefined ? E.left(NEA.of('年龄必填'))
: age < 18 ? E.left(NEA.of('必须年满18岁'))
: E.right(age)
// 组合所有验证 - 收集所有错误
const validateForm = (form: FormData) =>
sequenceS(validation)({
email: validateEmail(form.email),
password: validatePassword(form.password),
age: validateAge(form.age)
})
// 使用示例
validateForm({ email: '', password: '123', age: 15 })
// Left(['邮箱必填', '密码过短', '必须年满18岁'])
validateForm({ email: 'a@b.com', password: 'longpassword', age: 25 })
// Right({ email: 'a@b.com', password: 'longpassword', age: 25 })Field-Level Errors for Forms
表单的字段级错误
typescript
interface FieldError {
field: string
message: string
}
type FormErrors = NEA.NonEmptyArray<FieldError>
const fieldError = (field: string, message: string): FormErrors =>
NEA.of({ field, message })
const formValidation = E.getApplicativeValidation(NEA.getSemigroup<FieldError>())
// Now errors know which field they belong to
const validateEmail = (email: string): E.Either<FormErrors, string> =>
!email ? E.left(fieldError('email', 'Required'))
: !email.includes('@') ? E.left(fieldError('email', 'Invalid format'))
: E.right(email)
// Easy to display in UI
const getFieldError = (errors: FormErrors, field: string): string | undefined =>
errors.find(e => e.field === field)?.messagetypescript
interface FieldError {
field: string
message: string
}
type FormErrors = NEA.NonEmptyArray<FieldError>
const fieldError = (field: string, message: string): FormErrors =>
NEA.of({ field, message })
const formValidation = E.getApplicativeValidation(NEA.getSemigroup<FieldError>())
// 现在错误会关联到对应的字段
const validateEmail = (email: string): E.Either<FormErrors, string> =>
!email ? E.left(fieldError('email', '必填'))
: !email.includes('@') ? E.left(fieldError('email', '格式无效'))
: E.right(email)
// 轻松在UI中展示
const getFieldError = (errors: FormErrors, field: string): string | undefined =>
errors.find(e => e.field === field)?.message5. Async Operations (TaskEither)
5. 异步操作(TaskEither)
For async operations that can fail, use . It's like but for promises.
TaskEitherEither- = a function that returns
TaskEither<E, A>Promise<Either<E, A>> - Lazy: nothing runs until you execute it
typescript
import * as TE from 'fp-ts/TaskEither'
import { pipe } from 'fp-ts/function'
// Wrap any async operation
const fetchUser = (id: string): TE.TaskEither<Error, User> =>
TE.tryCatch(
() => fetch(`/api/users/${id}`).then(r => r.json()),
(e) => (e instanceof Error ? e : new Error(String(e)))
)
// Chain async operations - just like Either
const getUserPosts = (userId: string): TE.TaskEither<Error, Post[]> =>
pipe(
fetchUser(userId),
TE.chain(user => fetchPosts(user.id))
)
// Execute when ready
const result = await getUserPosts('123')() // Returns Either<Error, Post[]>对于可能失败的异步操作,使用。它就像是用于Promise的。
TaskEitherEither- = 一个返回
TaskEither<E, A>的函数Promise<Either<E, A>> - 惰性执行: 直到你调用它才会运行
typescript
import * as TE from 'fp-ts/TaskEither'
import { pipe } from 'fp-ts/function'
// 包装任何异步操作
const fetchUser = (id: string): TE.TaskEither<Error, User> =>
TE.tryCatch(
() => fetch(`/api/users/${id}`).then(r => r.json()),
(e) => (e instanceof Error ? e : new Error(String(e)))
)
// 链式调用异步操作 - 和Either的用法一样
const getUserPosts = (userId: string): TE.TaskEither<Error, Post[]> =>
pipe(
fetchUser(userId),
TE.chain(user => fetchPosts(user.id))
)
// 准备好后执行
const result = await getUserPosts('123')() // 返回Either<Error, Post[]>Before: Promise Chain with Error Handling
之前: 带错误处理的Promise链式调用
typescript
// MESSY: try/catch mixed with promise chains
async function loadDashboard(userId: string) {
try {
const user = await fetchUser(userId)
if (!user) throw new Error('User not found')
let posts, notifications, settings
try {
[posts, notifications, settings] = await Promise.all([
fetchPosts(user.id),
fetchNotifications(user.id),
fetchSettings(user.id)
])
} catch (e) {
// Which one failed? Who knows!
console.error('Failed to load data', e)
return null
}
return { user, posts, notifications, settings }
} catch (e) {
console.error('Failed to load user', e)
return null
}
}typescript
// 混乱不堪: try/catch和Promise链式调用混合在一起
async function loadDashboard(userId: string) {
try {
const user = await fetchUser(userId)
if (!user) throw new Error('用户未找到')
let posts, notifications, settings
try {
[posts, notifications, settings] = await Promise.all([
fetchPosts(user.id),
fetchNotifications(user.id),
fetchSettings(user.id)
])
} catch (e) {
// 不知道哪个操作失败了!
console.error('数据加载失败', e)
return null
}
return { user, posts, notifications, settings }
} catch (e) {
console.error('用户加载失败', e)
return null
}
}After: Clean TaskEither Pipeline
之后: 清晰的TaskEither流水线
typescript
import * as TE from 'fp-ts/TaskEither'
import { sequenceS } from 'fp-ts/Apply'
import { pipe } from 'fp-ts/function'
const loadDashboard = (userId: string) =>
pipe(
fetchUser(userId),
TE.chain(user =>
pipe(
// Parallel fetch with sequenceS
sequenceS(TE.ApplyPar)({
posts: fetchPosts(user.id),
notifications: fetchNotifications(user.id),
settings: fetchSettings(user.id)
}),
TE.map(data => ({ user, ...data }))
)
)
)
// Execute and handle both cases
pipe(
loadDashboard('123'),
TE.fold(
(error) => T.of(renderError(error)),
(data) => T.of(renderDashboard(data))
)
)()typescript
import * as TE from 'fp-ts/TaskEither'
import { sequenceS } from 'fp-ts/Apply'
import { pipe } from 'fp-ts/function'
const loadDashboard = (userId: string) =>
pipe(
fetchUser(userId),
TE.chain(user =>
pipe(
// 使用sequenceS并行获取数据
sequenceS(TE.ApplyPar)({
posts: fetchPosts(user.id),
notifications: fetchNotifications(user.id),
settings: fetchSettings(user.id)
}),
TE.map(data => ({ user, ...data }))
)
)
)
// 执行并处理两种情况
pipe(
loadDashboard('123'),
TE.fold(
(error) => T.of(renderError(error)),
(data) => T.of(renderDashboard(data))
)
)()Retry Failed Operations
重试失败的操作
typescript
import * as T from 'fp-ts/Task'
import * as TE from '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 backoff
const fetchWithRetry = retry(fetchUser('123'), 3, 1000)typescript
import * as T from 'fp-ts/Task'
import * as TE from '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)
)
)
// 最多重试3次,使用指数退避策略
const fetchWithRetry = retry(fetchUser('123'), 3, 1000)Fallback to Alternative
回退到备选方案
typescript
// Try cache first, fall back to API
const getUserData = (id: string) =>
pipe(
fetchFromCache(id),
TE.orElse(() => fetchFromApi(id)),
TE.orElse(() => TE.right(defaultUser)) // Last resort default
)typescript
// 先尝试缓存,失败则回退到API
const getUserData = (id: string) =>
pipe(
fetchFromCache(id),
TE.orElse(() => fetchFromApi(id)),
TE.orElse(() => TE.right(defaultUser)) // 最后的兜底默认值
)6. Converting Between Patterns
6. 模式之间的转换
Real codebases have throwing functions, nullable values, and promises. Here's how to work with them.
实际代码库中会存在抛出错误的函数、可空值和Promise。以下是如何在它们之间转换的方法。
From Nullable to Either
从可空值到Either
typescript
import * as E from 'fp-ts/Either'
import * as O from 'fp-ts/Option'
// Direct conversion
const user = users.find(u => u.id === id) // User | undefined
const result = E.fromNullable('User not found')(user)
// From Option
const maybeUser: O.Option<User> = O.fromNullable(user)
const eitherUser = pipe(
maybeUser,
E.fromOption(() => 'User not found')
)typescript
import * as E from 'fp-ts/Either'
import * as O from 'fp-ts/Option'
// 直接转换
const user = users.find(u => u.id === id) // User | undefined
const result = E.fromNullable('用户未找到')(user)
// 从Option转换
const maybeUser: O.Option<User> = O.fromNullable(user)
const eitherUser = pipe(
maybeUser,
E.fromOption(() => '用户未找到')
)From Throwing Function to Either
从抛出错误的函数到Either
typescript
// Wrap at the boundary
const safeParse = <T>(schema: ZodSchema<T>) => (data: unknown): E.Either<ZodError, T> =>
E.tryCatch(
() => schema.parse(data),
(e) => e as ZodError
)
// Use throughout your code
const parseUser = safeParse(UserSchema)
const result = parseUser(rawData) // Either<ZodError, User>typescript
// 在边界处进行包装
const safeParse = <T>(schema: ZodSchema<T>) => (data: unknown): E.Either<ZodError, T> =>
E.tryCatch(
() => schema.parse(data),
(e) => e as ZodError
)
// 在代码中统一使用
const parseUser = safeParse(UserSchema)
const result = parseUser(rawData) // Either<ZodError, User>From Promise to TaskEither
从Promise到TaskEither
typescript
import * as TE from 'fp-ts/TaskEither'
// Wrap external async functions
const fetchJson = <T>(url: string): TE.TaskEither<Error, T> =>
TE.tryCatch(
() => fetch(url).then(r => r.json()),
(e) => new Error(`Fetch failed: ${e}`)
)
// Wrap axios, prisma, any async library
const getUserFromDb = (id: string): TE.TaskEither<DbError, User> =>
TE.tryCatch(
() => prisma.user.findUniqueOrThrow({ where: { id } }),
(e) => ({ code: 'DB_ERROR', cause: e })
)typescript
import * as TE from 'fp-ts/TaskEither'
// 包装外部异步函数
const fetchJson = <T>(url: string): TE.TaskEither<Error, T> =>
TE.tryCatch(
() => fetch(url).then(r => r.json()),
(e) => new Error(`请求失败: ${e}`)
)
// 包装axios、prisma等任何异步库
const getUserFromDb = (id: string): TE.TaskEither<DbError, User> =>
TE.tryCatch(
() => prisma.user.findUniqueOrThrow({ where: { id } }),
(e) => ({ code: 'DB_ERROR', cause: e })
)Back to Promise (Escape Hatch)
转换回Promise(逃生舱)
Sometimes you need a plain Promise for external APIs.
typescript
import * as TE from 'fp-ts/TaskEither'
import * as E from 'fp-ts/Either'
const myTaskEither: TE.TaskEither<Error, User> = fetchUser('123')
// Option 1: Get the Either (preserves both cases)
const either: E.Either<Error, User> = await myTaskEither()
// Option 2: Throw on error (for legacy code)
const toThrowingPromise = <E, A>(te: TE.TaskEither<E, A>): Promise<A> =>
te().then(E.fold(
(error) => Promise.reject(error),
(value) => Promise.resolve(value)
))
const user = await toThrowingPromise(fetchUser('123')) // Throws if Left
// Option 3: Default on error
const user = await pipe(
fetchUser('123'),
TE.getOrElse(() => T.of(defaultUser))
)()有时你需要为外部API提供普通的Promise。
typescript
import * as TE from 'fp-ts/TaskEither'
import * as E from 'fp-ts/Either'
const myTaskEither: TE.TaskEither<Error, User> = fetchUser('123')
// 选项1: 获取Either(保留两种情况)
const either: E.Either<Error, User> = await myTaskEither()
// 选项2: 错误时抛出(用于遗留代码)
const toThrowingPromise = <E, A>(te: TE.TaskEither<E, A>): Promise<A> =>
te().then(E.fold(
(error) => Promise.reject(error),
(value) => Promise.resolve(value)
))
const user = await toThrowingPromise(fetchUser('123')) // 如果是Left则抛出错误
// 选项3: 错误时使用默认值
const user = await pipe(
fetchUser('123'),
TE.getOrElse(() => T.of(defaultUser))
)()Real Scenarios
实际场景
Parse User Input Safely
安全解析用户输入
typescript
interface ParsedInput {
id: number
name: string
tags: string[]
}
const parseInput = (raw: unknown): E.Either<string, ParsedInput> =>
pipe(
E.Do,
E.bind('obj', () =>
typeof raw === 'object' && raw !== null
? E.right(raw as Record<string, unknown>)
: E.left('Input must be an object')
),
E.bind('id', ({ obj }) =>
typeof obj.id === 'number'
? E.right(obj.id)
: E.left('id must be a number')
),
E.bind('name', ({ obj }) =>
typeof obj.name === 'string' && obj.name.length > 0
? E.right(obj.name)
: E.left('name must be a non-empty string')
),
E.bind('tags', ({ obj }) =>
Array.isArray(obj.tags) && obj.tags.every(t => typeof t === 'string')
? E.right(obj.tags as string[])
: E.left('tags must be an array of strings')
),
E.map(({ id, name, tags }) => ({ id, name, tags }))
)
// Usage
parseInput({ id: 1, name: 'test', tags: ['a', 'b'] })
// Right({ id: 1, name: 'test', tags: ['a', 'b'] })
parseInput({ id: 'wrong', name: '', tags: null })
// Left('id must be a number')typescript
interface ParsedInput {
id: number
name: string
tags: string[]
}
const parseInput = (raw: unknown): E.Either<string, ParsedInput> =>
pipe(
E.Do,
E.bind('obj', () =>
typeof raw === 'object' && raw !== null
? E.right(raw as Record<string, unknown>)
: E.left('输入必须是对象')
),
E.bind('id', ({ obj }) =>
typeof obj.id === 'number'
? E.right(obj.id)
: E.left('id必须是数字')
),
E.bind('name', ({ obj }) =>
typeof obj.name === 'string' && obj.name.length > 0
? E.right(obj.name)
: E.left('name必须是非空字符串')
),
E.bind('tags', ({ obj }) =>
Array.isArray(obj.tags) && obj.tags.every(t => typeof t === 'string')
? E.right(obj.tags as string[])
: E.left('tags必须是字符串数组')
),
E.map(({ id, name, tags }) => ({ id, name, tags }))
)
// 使用示例
parseInput({ id: 1, name: 'test', tags: ['a', 'b'] })
// Right({ id: 1, name: 'test', tags: ['a', 'b'] })
parseInput({ id: 'wrong', name: '', tags: null })
// Left('id必须是数字')API Call with Full Error Handling
带完整错误处理的API调用
typescript
interface ApiError {
code: string
message: string
status?: number
}
const createApiError = (message: string, code = 'UNKNOWN', status?: number): ApiError =>
({ code, message, status })
const fetchWithErrorHandling = <T>(url: string): TE.TaskEither<ApiError, T> =>
pipe(
TE.tryCatch(
() => fetch(url),
() => createApiError('Network error', 'NETWORK')
),
TE.chain(response =>
response.ok
? TE.tryCatch(
() => response.json() as Promise<T>,
() => createApiError('Invalid JSON', 'PARSE')
)
: TE.left(createApiError(
`HTTP ${response.status}`,
response.status === 404 ? 'NOT_FOUND' : 'HTTP_ERROR',
response.status
))
)
)
// Usage with pattern matching on error codes
const handleUserFetch = (userId: string) =>
pipe(
fetchWithErrorHandling<User>(`/api/users/${userId}`),
TE.fold(
(error) => {
switch (error.code) {
case 'NOT_FOUND': return T.of(showNotFoundPage())
case 'NETWORK': return T.of(showOfflineMessage())
default: return T.of(showGenericError(error.message))
}
},
(user) => T.of(showUserProfile(user))
)
)typescript
interface ApiError {
code: string
message: string
status?: number
}
const createApiError = (message: string, code = 'UNKNOWN', status?: number): ApiError =>
({ code, message, status })
const fetchWithErrorHandling = <T>(url: string): TE.TaskEither<ApiError, T> =>
pipe(
TE.tryCatch(
() => fetch(url),
() => createApiError('网络错误', 'NETWORK')
),
TE.chain(response =>
response.ok
? TE.tryCatch(
() => response.json() as Promise<T>,
() => createApiError('无效JSON', 'PARSE')
)
: TE.left(createApiError(
`HTTP ${response.status}`,
response.status === 404 ? 'NOT_FOUND' : 'HTTP_ERROR',
response.status
))
)
)
// 根据错误码进行模式匹配的使用示例
const handleUserFetch = (userId: string) =>
pipe(
fetchWithErrorHandling<User>(`/api/users/${userId}`),
TE.fold(
(error) => {
switch (error.code) {
case 'NOT_FOUND': return T.of(showNotFoundPage())
case 'NETWORK': return T.of(showOfflineMessage())
default: return T.of(showGenericError(error.message))
}
},
(user) => T.of(showUserProfile(user))
)
)Process List Where Some Items Might Fail
处理可能部分失败的列表
typescript
import * as A from 'fp-ts/Array'
import * as E from 'fp-ts/Either'
import { pipe } from 'fp-ts/function'
interface ProcessResult<T> {
successes: T[]
failures: Array<{ item: unknown; error: string }>
}
// Process all, collect successes and failures separately
const processAllCollectErrors = <T, R>(
items: T[],
process: (item: T) => E.Either<string, R>
): ProcessResult<R> => {
const results = items.map((item, index) =>
pipe(
process(item),
E.mapLeft(error => ({ item, error, index }))
)
)
return {
successes: pipe(results, A.filterMap(E.toOption)),
failures: pipe(
results,
A.filterMap(r => E.isLeft(r) ? O.some(r.left) : O.none)
)
}
}
// Usage
const parseNumbers = (inputs: string[]) =>
processAllCollectErrors(inputs, input => {
const n = parseInt(input, 10)
return isNaN(n) ? E.left(`Invalid number: ${input}`) : E.right(n)
})
parseNumbers(['1', 'abc', '3', 'def'])
// {
// successes: [1, 3],
// failures: [
// { item: 'abc', error: 'Invalid number: abc', index: 1 },
// { item: 'def', error: 'Invalid number: def', index: 3 }
// ]
// }typescript
import * as A from 'fp-ts/Array'
import * as E from 'fp-ts/Either'
import { pipe } from 'fp-ts/function'
interface ProcessResult<T> {
successes: T[]
failures: Array<{ item: unknown; error: string }>
}
// 处理所有项,分别收集成功和失败的结果
const processAllCollectErrors = <T, R>(
items: T[],
process: (item: T) => E.Either<string, R>
): ProcessResult<R> => {
const results = items.map((item, index) =>
pipe(
process(item),
E.mapLeft(error => ({ item, error, index }))
)
)
return {
successes: pipe(results, A.filterMap(E.toOption)),
failures: pipe(
results,
A.filterMap(r => E.isLeft(r) ? O.some(r.left) : O.none)
)
}
}
// 使用示例
const parseNumbers = (inputs: string[]) =>
processAllCollectErrors(inputs, input => {
const n = parseInt(input, 10)
return isNaN(n) ? E.left(`无效数字: ${input}`) : E.right(n)
})
parseNumbers(['1', 'abc', '3', 'def'])
// {
// successes: [1, 3],
// failures: [
// { item: 'abc', error: '无效数字: abc', index: 1 },
// { item: 'def', error: '无效数字: def', index: 3 }
// ]
// }Bulk Operations with Partial Success
部分成功的批量操作
typescript
import * as TE from 'fp-ts/TaskEither'
import * as T from 'fp-ts/Task'
import { pipe } from 'fp-ts/function'
interface BulkResult<T> {
succeeded: T[]
failed: Array<{ id: string; error: string }>
}
const bulkProcess = <T>(
ids: string[],
process: (id: string) => TE.TaskEither<string, T>
): T.Task<BulkResult<T>> =>
pipe(
ids,
A.map(id =>
pipe(
process(id),
TE.fold(
(error) => T.of({ type: 'failed' as const, id, error }),
(result) => T.of({ type: 'succeeded' as const, result })
)
)
),
T.sequenceArray,
T.map(results => ({
succeeded: results
.filter((r): r is { type: 'succeeded'; result: T } => r.type === 'succeeded')
.map(r => r.result),
failed: results
.filter((r): r is { type: 'failed'; id: string; error: string } => r.type === 'failed')
.map(({ id, error }) => ({ id, error }))
}))
)
// Usage
const deleteUsers = (userIds: string[]) =>
bulkProcess(userIds, id =>
pipe(
deleteUser(id),
TE.mapLeft(e => e.message)
)
)
// All operations run, you get a report of what worked and what didn'ttypescript
import * as TE from 'fp-ts/TaskEither'
import * as T from 'fp-ts/Task'
import { pipe } from 'fp-ts/function'
interface BulkResult<T> {
succeeded: T[]
failed: Array<{ id: string; error: string }>
}
const bulkProcess = <T>(
ids: string[],
process: (id: string) => TE.TaskEither<string, T>
): T.Task<BulkResult<T>> =>
pipe(
ids,
A.map(id =>
pipe(
process(id),
TE.fold(
(error) => T.of({ type: 'failed' as const, id, error }),
(result) => T.of({ type: 'succeeded' as const, result })
)
)
),
T.sequenceArray,
T.map(results => ({
succeeded: results
.filter((r): r is { type: 'succeeded'; result: T } => r.type === 'succeeded')
.map(r => r.result),
failed: results
.filter((r): r is { type: 'failed'; id: string; error: string } => r.type === 'failed')
.map(({ id, error }) => ({ id, error }))
}))
)
// 使用示例
const deleteUsers = (userIds: string[]) =>
bulkProcess(userIds, id =>
pipe(
deleteUser(id),
TE.mapLeft(e => e.message)
)
)
// 所有操作都会执行,你会得到一份成功和失败的报告Quick Reference
速查表
| Pattern | Use When | Example |
|---|---|---|
| Creating a success | |
| Creating a failure | |
| Wrapping throwing code | |
| Converting nullable | |
| Transform success | |
| Transform error | |
| Chain operations | |
| Chain with different error type | |
| Handle both cases | |
| Extract with default | |
| Validate with error | |
| Collect all errors | Form validation |
| 模式 | 使用场景 | 示例 |
|---|---|---|
| 创建成功结果 | |
| 创建失败结果 | |
| 包装抛出错误的代码 | |
| 转换可空值 | |
| 转换成功值 | |
| 转换错误信息 | |
| 链式调用操作 | |
| 链式调用不同错误类型的操作 | |
| 处理两种结果情况 | |
| 提取值并提供默认值 | |
| 带错误返回的验证 | |
| 收集所有错误 | 表单验证 |
TaskEither Equivalents
TaskEither等价操作
All Either operations have TaskEither equivalents:
- ,
TE.right,TE.leftTE.tryCatch - ,
TE.map,TE.mapLeft,TE.chainTE.chainW - ,
TE.fold,TE.getOrElseTE.filterOrElse - for fallbacks
TE.orElse
所有Either的操作都有对应的TaskEither版本:
- ,
TE.right,TE.leftTE.tryCatch - ,
TE.map,TE.mapLeft,TE.chainTE.chainW - ,
TE.fold,TE.getOrElseTE.filterOrElse - 用于回退方案
TE.orElse
Summary
总结
- Return errors as values - Use Either/TaskEither instead of throwing
- Chain with confidence - stops at first error automatically
chain - Collect all errors when needed - Use validation applicative for forms
- Wrap at boundaries - Convert throwing/Promise code at the edges
- Match at the end - Use to handle both cases when you're ready to act
fold
The payoff: TypeScript tracks your errors, no more forgotten try/catch, clear control flow, and composable error handling.
- 将错误作为值返回 - 使用Either/TaskEither替代抛出错误
- 放心地链式调用 - 会在第一个错误处自动终止流程
chain - 需要时收集所有错误 - 对表单验证使用validation applicative
- 在边界处包装 - 在代码边缘转换抛出错误/Promise的代码
- 在最后进行匹配 - 当你准备好处理结果时使用处理两种情况
fold
回报是:TypeScript会追踪你的错误,不再有被遗忘的try/catch,清晰的控制流,以及可组合的错误处理。