neverthrow,ts 中优雅的异常处理方案

最后所有的错误都写成了 unknown,最后项目变成了 unknownscript,看了半天才能捋清楚项目的链路

为什么传统 try/catch 让人头疼?

先看一个典型的 TypeScript 函数:

js 复制代码
function getUser(id: string): User | undefined {
  try {
    const user = db.findUserById(id)
    return user
  } catch (e) {
    console.error(e)
    return undefined
  }
}

问题很明显:

  1. 错误类型不透明
    catch 到的 e 在 TypeScript 里是 unknown,你无法确定它到底是什么。是数据库错误?网络错误?还是业务校验失败?必须写一堆 instanceof 判断。
  2. 函数签名不体现失败
    getUser 的返回值是 User | undefined,但调用者并不知道"失败"有多少种可能,也不知道什么时候该处理错误。
  3. 控制流被打断
    try/catch 像一个安全网,一旦出错就跳出去,很难把多个可能失败的操作串成一条清晰的管道。
  4. 异步场景更痛苦
    async/await 虽然让异步代码变同步,但错误处理依然要靠 try/catch,多个异步调用嵌套时,代码可读性直线下降。

neverthrow 的哲学:错误不是异常,而是值

neverthrow 是一个轻量级的 TypeScript 库,它引入了函数式编程中的 Result 类型,帮助你把错误作为值来返回,而不是抛出异常

核心类型只有两个:

  • Result<T, E>:表示一个操作可能成功(Ok<T>)也可能失败(Err<E>
  • ResultAsync<T, E>:异步版本的 Result

通过显式地标记成功和失败类型,编译器会强制你处理所有可能的错误路径,从而在编译期就消灭大量运行时隐患。

快速上手

bash 复制代码
npm install neverthrow

同步场景:安全解析 JSON

js 复制代码
import { ok, err, Result } from 'neverthrow'

type ParseError = { message: string }

function parseJSON<T>(json: string): Result<T, ParseError> {
  try {
    return ok(JSON.parse(json) as T)
  } catch {
    return err({ message: 'Invalid JSON' })
  }
}

// 使用
const result = parseJSON<User>('{"name":"Alice"}')

result.match(
  user => console.log(user.name),        // 成功分支
  error => console.error(error.message)  // 失败分支
)

注意:函数签名 Result<T, ParseError> 明确告诉调用者"成功返回 T,失败返回 ParseError",不再需要猜测。

异步场景:请求接口

js 复制代码
import { fromPromise, ResultAsync } from 'neverthrow'

type ApiError = { status: number; message: string }

function fetchUser(id: string): ResultAsync<User, ApiError> {
  return fromPromise(
    fetch(`/api/users/${id}`).then(res => {
      if (!res.ok) {
        return Promise.reject({ status: res.status, message: 'Failed to fetch' })
      }
      return res.json() as Promise<User>
    }),
    e => e as ApiError
  )
}

// 使用
fetchUser('123').match(
  user => console.log(user.name),
  error => console.error(`${error.status}: ${error.message}`)
)

组合多个可能失败的操作

neverthrow 真正强大的地方在于可组合性 。你可以像管道一样把多个 Result 串联起来:

js 复制代码
type ValidationError = string

function validateEmail(email: string): Result<string, ValidationError> {
  if (!email.includes('@')) {
    return err('Invalid email')
  }
  return ok(email)
}

function normalizeEmail(email: string): Result<string, ValidationError> {
  return ok(email.trim().toLowerCase())
}

const result = validateEmail('  Foo@Bar.COM  ')
  .andThen(normalizeEmail)
  .map(email => ({ email }))

result.match(
  data => console.log(data.email), // "foo@bar.com"
  error => console.error(error)
)

这里 andThen 只有在前面成功时才会执行下一个函数,map 则用来转换成功值。整个流程线性、清晰、无嵌套。

太多了,neverthrow 最大的优势就是需要用户明确定义错误类型,这种处理在代码链路复杂的项目中非常有用.

实战建议

  • 团队需要统一约定 :如果代码库中一部分使用 throw,一部分使用 Result,会让风格混乱。建议在新模块或新项目中全面采用。
  • 错误类型需要设计 :如果到处定义临时错误类型,反而增加维护成本。建议在项目层面定义统一的 AppError 类型。
  • 与旧代码共存 :对于仍然抛出异常的第三方库,使用 fromThrowablefromPromise 包装一层即可。
相关推荐
烬羽1 小时前
求第 K 大,为什么反而要用最小堆?
javascript·数据结构·算法
jingchao19981 小时前
Cannot read properties of null (reading ‘insertBefore‘)
前端·javascript·vue.js
计算机魔术师1 小时前
新文章
前端
爱跳舞的烤冷面1 小时前
自学嵌入式第N天(Linux篇——文件编程)
java·开发语言·前端
IT_陈寒1 小时前
Vite的热更新突然失效,原来我忽略了这个配置
前端·人工智能·后端
怪奇云呼军1 小时前
闪电智能VoiceAgent 如何管理呼入、接听、桥接和挂断状态?
java·前端·网络·数据库·人工智能
云浪1 小时前
Milvus + RAG 实战:《红楼梦》问答助手
前端·人工智能·后端
梦醒沉醉2 小时前
2、JavaScript控制流和错误处理
javascript
恋猫de小郭2 小时前
Dart 3.13 大更新,感觉比 Flutter 更带劲
android·前端·flutter