最后所有的错误都写成了 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
}
}
问题很明显:
- 错误类型不透明
catch到的e在 TypeScript 里是unknown,你无法确定它到底是什么。是数据库错误?网络错误?还是业务校验失败?必须写一堆instanceof判断。 - 函数签名不体现失败
getUser的返回值是User | undefined,但调用者并不知道"失败"有多少种可能,也不知道什么时候该处理错误。 - 控制流被打断
try/catch像一个安全网,一旦出错就跳出去,很难把多个可能失败的操作串成一条清晰的管道。 - 异步场景更痛苦
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类型。 - 与旧代码共存 :对于仍然抛出异常的第三方库,使用
fromThrowable或fromPromise包装一层即可。