fp-ts-errors

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Practical 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 both

typescript
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>
is simple: it holds either an error (
E
) or a value (
A
).
  • Left
    = error case
  • Right
    = success case (think "right" as in "correct")
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>
很简单:它要么包含一个错误(
E
),要么包含一个值(
A
)。
  • 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/undefined

typescript
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)?.message

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>())

// 现在错误会关联到对应的字段
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)?.message

5. Async Operations (TaskEither)

5. 异步操作(TaskEither)

For async operations that can fail, use
TaskEither
. It's like
Either
but for promises.
  • TaskEither<E, A>
    = a function that returns
    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[]>
对于可能失败的异步操作,使用
TaskEither
。它就像是用于Promise的
Either
  • 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't

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 }))
    }))
  )

// 使用示例
const deleteUsers = (userIds: string[]) =>
  bulkProcess(userIds, id =>
    pipe(
      deleteUser(id),
      TE.mapLeft(e => e.message)
    )
  )

// 所有操作都会执行,你会得到一份成功和失败的报告

Quick Reference

速查表

PatternUse WhenExample
E.right(value)
Creating a success
E.right(42)
E.left(error)
Creating a failure
E.left('not found')
E.tryCatch(fn, onError)
Wrapping throwing code
E.tryCatch(() => JSON.parse(s), toError)
E.fromNullable(error)
Converting nullable
E.fromNullable('missing')(maybeValue)
E.map(fn)
Transform success
pipe(result, E.map(x => x * 2))
E.mapLeft(fn)
Transform error
pipe(result, E.mapLeft(addContext))
E.chain(fn)
Chain operations
pipe(getA(), E.chain(a => getB(a.id)))
E.chainW(fn)
Chain with different error type
pipe(validate(), E.chainW(save))
E.fold(onError, onSuccess)
Handle both cases
E.fold(showError, showData)
E.getOrElse(onError)
Extract with default
E.getOrElse(() => 0)
E.filterOrElse(pred, onFalse)
Validate with error
E.filterOrElse(x => x > 0, () => 'must be positive')
sequenceS(validation)({...})
Collect all errorsForm validation
模式使用场景示例
E.right(value)
创建成功结果
E.right(42)
E.left(error)
创建失败结果
E.left('未找到')
E.tryCatch(fn, onError)
包装抛出错误的代码
E.tryCatch(() => JSON.parse(s), toError)
E.fromNullable(error)
转换可空值
E.fromNullable('缺失')(maybeValue)
E.map(fn)
转换成功值
pipe(result, E.map(x => x * 2))
E.mapLeft(fn)
转换错误信息
pipe(result, E.mapLeft(addContext))
E.chain(fn)
链式调用操作
pipe(getA(), E.chain(a => getB(a.id)))
E.chainW(fn)
链式调用不同错误类型的操作
pipe(validate(), E.chainW(save))
E.fold(onError, onSuccess)
处理两种结果情况
E.fold(showError, showData)
E.getOrElse(onError)
提取值并提供默认值
E.getOrElse(() => 0)
E.filterOrElse(pred, onFalse)
带错误返回的验证
E.filterOrElse(x => x > 0, () => '必须为正数')
sequenceS(validation)({...})
收集所有错误表单验证

TaskEither Equivalents

TaskEither等价操作

All Either operations have TaskEither equivalents:
  • TE.right
    ,
    TE.left
    ,
    TE.tryCatch
  • TE.map
    ,
    TE.mapLeft
    ,
    TE.chain
    ,
    TE.chainW
  • TE.fold
    ,
    TE.getOrElse
    ,
    TE.filterOrElse
  • TE.orElse
    for fallbacks

所有Either的操作都有对应的TaskEither版本:
  • TE.right
    ,
    TE.left
    ,
    TE.tryCatch
  • TE.map
    ,
    TE.mapLeft
    ,
    TE.chain
    ,
    TE.chainW
  • TE.fold
    ,
    TE.getOrElse
    ,
    TE.filterOrElse
  • TE.orElse
    用于回退方案

Summary

总结

  1. Return errors as values - Use Either/TaskEither instead of throwing
  2. Chain with confidence -
    chain
    stops at first error automatically
  3. Collect all errors when needed - Use validation applicative for forms
  4. Wrap at boundaries - Convert throwing/Promise code at the edges
  5. Match at the end - Use
    fold
    to handle both cases when you're ready to act
The payoff: TypeScript tracks your errors, no more forgotten try/catch, clear control flow, and composable error handling.
  1. 将错误作为值返回 - 使用Either/TaskEither替代抛出错误
  2. 放心地链式调用 -
    chain
    会在第一个错误处自动终止流程
  3. 需要时收集所有错误 - 对表单验证使用validation applicative
  4. 在边界处包装 - 在代码边缘转换抛出错误/Promise的代码
  5. 在最后进行匹配 - 当你准备好处理结果时使用
    fold
    处理两种情况
回报是:TypeScript会追踪你的错误,不再有被遗忘的try/catch,清晰的控制流,以及可组合的错误处理。