Effect 与 neverthrow 对比
Effect 与 neverthrow 的对比,涵盖类型安全与错误处理等特性。
在 TypeScript 中处理错误时,neverthrow 与 Effect 都提供了有用的抽象,
用来在不使用异常的情况下对成功与失败建模。二者共享许多概念,例如把计算包装进一个安全的容器、
用 map 转换值、用 mapErr/mapLeft 处理错误,以及提供组合或解包结果的工具函数。
本页针对常见用例,对 neverthrow 与 Effect 的 API 做了并排对比。 如果你已经熟悉 neverthrow,这些示例会帮助你理解如何用 Effect 实现同样的模式。 如果你是初次接触,这份对比会突出二者的相似与不同之处,帮助你判断哪个库更适合你的项目。
neverthrow 暴露的是实例方法(例如 result.map(...))。
Effect 暴露的是 Either 上的函数(例如 Either.map(result, ...)),并支持 pipe 风格,以获得更好的可读性和更好的 tree shaking。
同步 API
ok
示例(创建成功结果)
import { ok } from "neverthrow"
const result = ok({ myData: "test" })
result.isOk() // true
result.isErr() // falseimport * as Either from "effect/Either"
const result = Either.right({ myData: "test" })
Either.isRight(result) // true
Either.isLeft(result) // falseerr
示例(创建失败结果)
import { err } from "neverthrow"
const result = err("Oh no")
result.isOk() // false
result.isErr() // trueimport * as Either from "effect/Either"
const result = Either.left("Oh no")
Either.isRight(result) // false
Either.isLeft(result) // truemap
示例(转换成功值)
import { Result } from "neverthrow"
declare function getLines(s: string): Result<Array<string>, Error>
const result = getLines("1\n2\n3\n4\n")
// this Result now has a Array<number> inside it
const newResult = result.map((arr) => arr.map(parseInt))
newResult.isOk() // trueimport * as Either from "effect/Either"
declare function getLines(s: string): Either.Either<Array<string>, Error>
const result = getLines("1\n2\n3\n4\n")
// this Either now has a Array<number> inside it
const newResult = result.pipe(Either.map((arr) => arr.map(parseInt)))
Either.isRight(newResult) // truemapErr
示例(转换错误值)
import { Result } from "neverthrow"
declare function parseHeaders(
raw: string,
): Result<Record<string, string>, string>
const rawHeaders = "nonsensical gibberish and badly formatted stuff"
const result = parseHeaders(rawHeaders)
// const newResult: Result<Record<string, string>, Error>
const newResult = result.mapErr((err) => new Error(err))import * as Either from "effect/Either"
declare function parseHeaders(
raw: string,
): Either.Either<Record<string, string>, string>
const rawHeaders = "nonsensical gibberish and badly formatted stuff"
const result = parseHeaders(rawHeaders)
// const newResult: Either<Record<string, string>, Error>
const newResult = result.pipe(Either.mapLeft((err) => new Error(err)))unwrapOr
示例(提供默认值)
import { err } from "neverthrow"
const result = err("Oh no")
const multiply = (value: number): number => value * 2
const unwrapped = result.map(multiply).unwrapOr(10)import * as Either from "effect/Either"
const result = Either.left("Oh no")
const multiply = (value: number): number => value * 2
const unwrapped = result.pipe(
Either.map(multiply),
Either.getOrElse(() => 10),
)andThen
示例(串联可能失败的计算)
import { ok, Result, err } from "neverthrow"
const sqrt = (n: number): Result<number, string> =>
n > 0 ? ok(Math.sqrt(n)) : err("n must be positive")
ok(16).andThen(sqrt).andThen(sqrt)
// Ok(2)import * as Either from "effect/Either"
const sqrt = (n: number): Either.Either<number, string> =>
n > 0 ? Either.right(Math.sqrt(n)) : Either.left("n must be positive")
Either.right(16).pipe(Either.andThen(sqrt), Either.andThen(sqrt))
// Right(2)asyncAndThen
示例(串联可能失败的异步计算)
import { ok, okAsync } from "neverthrow"
// const result: ResultAsync<number, never>
const result = ok(1).asyncAndThen((n) => okAsync(n + 1))import * as Either from "effect/Either"
import * as Effect from "effect/Effect"
// const result: Effect<number, never, never>
const result = Either.right(1).pipe(
Effect.andThen((n) => Effect.succeed(n + 1)),
)orElse
示例(在失败时提供备选方案)
import { Result, err, ok } from "neverthrow"
enum DatabaseError {
PoolExhausted = "PoolExhausted",
NotFound = "NotFound",
}
const dbQueryResult: Result<string, DatabaseError> = err(DatabaseError.NotFound)
const updatedQueryResult = dbQueryResult.orElse((dbError) =>
dbError === DatabaseError.NotFound ? ok("User does not exist") : err(500),
)import * as Either from "effect/Either"
enum DatabaseError {
PoolExhausted = "PoolExhausted",
NotFound = "NotFound",
}
const dbQueryResult: Either.Either<string, DatabaseError> = Either.left(
DatabaseError.NotFound,
)
const updatedQueryResult = dbQueryResult.pipe(
Either.orElse((dbError) =>
dbError === DatabaseError.NotFound
? Either.right("User does not exist")
: Either.left(500),
),
)match
示例(对成功或失败进行模式匹配)
import { Result } from "neverthrow"
declare const myResult: Result<number, string>
myResult.match(
(value) => `The value is ${value}`,
(error) => `The error is ${error}`,
)import * as Either from "effect/Either"
declare const myResult: Either.Either<number, string>
myResult.pipe(
Either.match({
onLeft: (error) => `The error is ${error}`,
onRight: (value) => `The value is ${value}`,
}),
)asyncMap
示例(解析请求头并查找用户)
import { Result } from "neverthrow"
interface User {}
declare function parseHeaders(
raw: string,
): Result<Record<string, string>, string>
declare function findUserInDatabase(
authorization: string,
): Promise<User | undefined>
const rawHeader = "Authorization: Bearer 1234567890"
// const asyncResult: ResultAsync<User | undefined, string>
const asyncResult = parseHeaders(rawHeader)
.map((kvMap) => kvMap["Authorization"])
.asyncMap((authorization) =>
authorization === undefined
? Promise.resolve(undefined)
: findUserInDatabase(authorization),
)import * as Either from "effect/Either"
import * as Effect from "effect/Effect"
interface User {}
declare function parseHeaders(
raw: string,
): Either.Either<Record<string, string>, string>
declare function findUserInDatabase(
authorization: string,
): Promise<User | undefined>
const rawHeader = "Authorization: Bearer 1234567890"
// const asyncResult: Effect<User | undefined, string | UnknownException>
const asyncResult = parseHeaders(rawHeader).pipe(
Either.map((kvMap) => kvMap["Authorization"]),
Effect.andThen((authorization) =>
authorization === undefined
? Promise.resolve(undefined)
: findUserInDatabase(authorization),
),
)注意。在 neverthrow 中,asyncMap 直接与 Promise 配合工作。
在 Effect 中,把 Promise 传给 Effect.andThen 这类组合子时,它会自动被提升为一个 Effect。
如果这个 Promise 被拒绝,该拒绝会被转换为一个 UnknownException,这就是错误类型被拓宽为 string | UnknownException 的原因。
combine
示例(合并多个结果)
import { Result, ok } from "neverthrow"
const results: Result<number, string>[] = [ok(1), ok(2)]
// const combined: Result<number[], string>
const combined = Result.combine(results)import * as Either from "effect/Either"
const results: Either.Either<number, string>[] = [
Either.right(1),
Either.right(2),
]
// const combined: Either<number[], string>
const combined = Either.all(results)combineWithAllErrors
示例(收集所有错误与成功值)
import { Result, ok, err } from "neverthrow"
const results: Result<number, string>[] = [
ok(123),
err("boooom!"),
ok(456),
err("ahhhhh!"),
]
const result = Result.combineWithAllErrors(results)
// result is Err(['boooom!', 'ahhhhh!'])import * as Either from "effect/Either"
import * as Array from "effect/Array"
const results: Either.Either<number, string>[] = [
Either.right(123),
Either.left("boooom!"),
Either.right(456),
Either.left("ahhhhh!"),
]
const errors = Array.getLefts(results)
// errors is ['boooom!', 'ahhhhh!']
const successes = Array.getRights(results)
// successes is [123, 456]注意。Effect 中没有与 Result.combineWithAllErrors 完全对应的函数。
可以用 Array.getLefts 收集所有错误,用 Array.getRights 收集所有成功值。
异步 API
下面的示例中,我们用 Effect.runPromise 运行一个 effect 并返回 Promise。
你也可以使用其他 API,例如 Effect.runPromiseExit,它能额外捕获 defect(运行时错误)和中断等情况。
okAsync
示例(创建一个成功的异步结果)
import { okAsync } from "neverthrow"
const myResultAsync = okAsync({ myData: "test" })
const result = await myResultAsync
result.isOk() // true
result.isErr() // falseimport * as Either from "effect/Either"
import * as Effect from "effect/Effect"
const myResultAsync = Effect.succeed({ myData: "test" })
const result = await Effect.runPromise(Effect.either(myResultAsync))
Either.isRight(result) // true
Either.isLeft(result) // falseerrAsync
示例(创建一个失败的异步结果)
import { errAsync } from "neverthrow"
const myResultAsync = errAsync("Oh no")
const myResult = await myResultAsync
myResult.isOk() // false
myResult.isErr() // trueimport * as Either from "effect/Either"
import * as Effect from "effect/Effect"
const myResultAsync = Effect.fail("Oh no")
const result = await Effect.runPromise(Effect.either(myResultAsync))
Either.isRight(result) // false
Either.isLeft(result) // truefromThrowable
示例(包装一个可能抛错、返回 Promise 的函数)
import { ResultAsync } from "neverthrow"
interface User {}
declare function insertIntoDb(user: User): Promise<User>
// (user: User) => ResultAsync<User, Error>
const insertUser = ResultAsync.fromThrowable(
insertIntoDb,
() => new Error("Database error"),
)import * as Effect from "effect/Effect"
interface User {}
declare function insertIntoDb(user: User): Promise<User>
// (user: User) => Effect<User, Error>
const insertUser = (user: User) =>
Effect.tryPromise({
try: () => insertIntoDb(user),
catch: () => new Error("Database error"),
})map
示例(转换成功值)
import { Result, ResultAsync } from "neverthrow"
interface User {
readonly name: string
}
declare function findUsersIn(country: string): ResultAsync<Array<User>, Error>
const usersInCanada = findUsersIn("Canada")
const namesInCanada = usersInCanada.map((users: Array<User>) =>
users.map((user) => user.name),
)
// We can extract the Result using .then() or await
namesInCanada.then((namesResult: Result<Array<string>, Error>) => {
if (namesResult.isErr()) {
console.log("Couldn't get the users from the database", namesResult.error)
} else {
console.log("Users in Canada are named: " + namesResult.value.join(","))
}
})import * as Effect from "effect/Effect"
import * as Either from "effect/Either"
interface User {
readonly name: string
}
declare function findUsersIn(country: string): Effect.Effect<Array<User>, Error>
const usersInCanada = findUsersIn("Canada")
const namesInCanada = usersInCanada.pipe(
Effect.map((users: Array<User>) => users.map((user) => user.name)),
)
// We can extract the Either using Effect.either
Effect.runPromise(Effect.either(namesInCanada)).then(
(namesResult: Either.Either<Array<string>, Error>) => {
if (Either.isLeft(namesResult)) {
console.log("Couldn't get the users from the database", namesResult.left)
} else {
console.log("Users in Canada are named: " + namesResult.right.join(","))
}
},
)mapErr
示例(转换错误值)
import { Result, ResultAsync } from "neverthrow"
interface User {
readonly name: string
}
declare function findUsersIn(country: string): ResultAsync<Array<User>, Error>
const usersInCanada = findUsersIn("Canada").mapErr((error: Error) => {
// The only error we want to pass to the user is "Unknown country"
if (error.message === "Unknown country") {
return error.message
}
// All other errors will be labelled as a system error
return "System error, please contact an administrator."
})
usersInCanada.then((usersResult: Result<Array<User>, string>) => {
if (usersResult.isErr()) {
console.log("Couldn't get the users from the database", usersResult.error)
} else {
console.log("Users in Canada are: " + usersResult.value.join(","))
}
})import * as Effect from "effect/Effect"
import * as Either from "effect/Either"
interface User {
readonly name: string
}
declare function findUsersIn(country: string): Effect.Effect<Array<User>, Error>
const usersInCanada = findUsersIn("Canada").pipe(
Effect.mapError((error: Error) => {
// The only error we want to pass to the user is "Unknown country"
if (error.message === "Unknown country") {
return error.message
}
// All other errors will be labelled as a system error
return "System error, please contact an administrator."
}),
)
Effect.runPromise(Effect.either(usersInCanada)).then(
(usersResult: Either.Either<Array<User>, string>) => {
if (Either.isLeft(usersResult)) {
console.log("Couldn't get the users from the database", usersResult.left)
} else {
console.log("Users in Canada are: " + usersResult.right.join(","))
}
},
)unwrapOr
示例(异步失败时提供默认值)
import { errAsync } from "neverthrow"
const unwrapped = await errAsync(0).unwrapOr(10)
// unwrapped = 10import * as Effect from "effect/Effect"
const unwrapped = await Effect.runPromise(
Effect.fail(0).pipe(Effect.orElseSucceed(() => 10)),
)
// unwrapped = 10andThen
示例(串联多个异步计算)
import { Result, ResultAsync } from "neverthrow"
interface User {}
declare function validateUser(user: User): ResultAsync<User, Error>
declare function insertUser(user: User): ResultAsync<User, Error>
declare function sendNotification(user: User): ResultAsync<void, Error>
const user: User = {}
const resAsync = validateUser(user)
.andThen(insertUser)
.andThen(sendNotification)
resAsync.then((res: Result<void, Error>) => {
if (res.isErr()) {
console.log("Oops, at least one step failed", res.error)
} else {
console.log("User has been validated, inserted and notified successfully.")
}
})import * as Effect from "effect/Effect"
import * as Either from "effect/Either"
interface User {}
declare function validateUser(user: User): Effect.Effect<User, Error>
declare function insertUser(user: User): Effect.Effect<User, Error>
declare function sendNotification(user: User): Effect.Effect<void, Error>
const user: User = {}
const resAsync = validateUser(user).pipe(
Effect.andThen(insertUser),
Effect.andThen(sendNotification),
)
Effect.runPromise(Effect.either(resAsync)).then(
(res: Either.Either<void, Error>) => {
if (Either.isLeft(res)) {
console.log("Oops, at least one step failed", res.left)
} else {
console.log(
"User has been validated, inserted and notified successfully.",
)
}
},
)orElse
示例(异步操作失败时回退)
import { ResultAsync, ok } from "neverthrow"
interface User {}
declare function fetchUserData(id: string): ResultAsync<User, Error>
declare function getDefaultUser(): User
const userId = "123"
// Try to fetch user data, but provide a default if it fails
const userResult = fetchUserData(userId).orElse(() => ok(getDefaultUser()))
userResult.then((result) => {
if (result.isOk()) {
console.log("User data:", result.value)
}
})import * as Effect from "effect/Effect"
import * as Either from "effect/Either"
interface User {}
declare function fetchUserData(id: string): Effect.Effect<User, Error>
declare function getDefaultUser(): User
const userId = "123"
// Try to fetch user data, but provide a default if it fails
const userResult = fetchUserData(userId).pipe(
Effect.orElse(() => Effect.succeed(getDefaultUser())),
)
Effect.runPromise(Effect.either(userResult)).then((result) => {
if (Either.isRight(result)) {
console.log("User data:", result.right)
}
})match
示例(在链的末尾处理成功与失败)
import { ResultAsync } from "neverthrow"
interface User {
readonly name: string
}
declare function validateUser(user: User): ResultAsync<User, Error>
declare function insertUser(user: User): ResultAsync<User, Error>
const user: User = { name: "John" }
// Handle both cases at the end of the chain using match
const resultMessage = await validateUser(user)
.andThen(insertUser)
.match(
(user: User) => `User ${user.name} has been successfully created`,
(error: Error) => `User could not be created because ${error.message}`,
)import * as Effect from "effect/Effect"
interface User {
readonly name: string
}
declare function validateUser(user: User): Effect.Effect<User, Error>
declare function insertUser(user: User): Effect.Effect<User, Error>
const user: User = { name: "John" }
// Handle both cases at the end of the chain using match
const resultMessage = await Effect.runPromise(
validateUser(user).pipe(
Effect.andThen(insertUser),
Effect.match({
onSuccess: (user) => `User ${user.name} has been successfully created`,
onFailure: (error) =>
`User could not be created because ${error.message}`,
}),
),
)combine
示例(组合多个异步结果)
import { ResultAsync, okAsync } from "neverthrow"
const resultList: ResultAsync<number, string>[] = [okAsync(1), okAsync(2)]
// const combinedList: ResultAsync<number[], string>
const combinedList = ResultAsync.combine(resultList)import * as Effect from "effect/Effect"
const resultList: Effect.Effect<number, string>[] = [
Effect.succeed(1),
Effect.succeed(2),
]
// const combinedList: Effect<number[], string>
const combinedList = Effect.all(resultList)combineWithAllErrors
示例(收集所有错误,而不是快速失败)
import { ResultAsync, okAsync, errAsync } from "neverthrow"
const resultList: ResultAsync<number, string>[] = [
okAsync(123),
errAsync("boooom!"),
okAsync(456),
errAsync("ahhhhh!"),
]
const result = await ResultAsync.combineWithAllErrors(resultList)
// result is Err(['boooom!', 'ahhhhh!'])import { Effect, identity } from "effect"
const resultList: Effect.Effect<number, string>[] = [
Effect.succeed(123),
Effect.fail("boooom!"),
Effect.succeed(456),
Effect.fail("ahhhhh!"),
]
const result = await Effect.runPromise(
Effect.either(Effect.validateAll(resultList, identity)),
)
// result is left(['boooom!', 'ahhhhh!'])实用工具
fromThrowable
示例(安全地包装一个会抛出异常的函数)
import { Result } from "neverthrow"
type ParseError = { message: string }
const toParseError = (): ParseError => ({ message: "Parse Error" })
const safeJsonParse = Result.fromThrowable(JSON.parse, toParseError)
// the function can now be used safely,
// if the function throws, the result will be an Err
const result = safeJsonParse("{")import * as Either from "effect/Either"
type ParseError = { message: string }
const toParseError = (): ParseError => ({ message: "Parse Error" })
const safeJsonParse = (s: string) =>
Either.try({ try: () => JSON.parse(s), catch: toParseError })
// the function can now be used safely,
// if the function throws, the result will be an Either
const result = safeJsonParse("{")safeTry
示例(用生成器简化错误处理)
import { Result, ok, safeTry } from "neverthrow"
declare function mayFail1(): Result<number, string>
declare function mayFail2(): Result<number, string>
function myFunc(): Result<number, string> {
return safeTry<number, string>(function* () {
return ok(
(yield* mayFail1().mapErr(
(e) => `aborted by an error from 1st function, ${e}`,
)) +
(yield* mayFail2().mapErr(
(e) => `aborted by an error from 2nd function, ${e}`,
)),
)
})
}import * as Either from "effect/Either"
declare function mayFail1(): Either.Either<number, string>
declare function mayFail2(): Either.Either<number, string>
function myFunc(): Either.Either<number, string> {
return Either.gen(function* () {
return (
(yield* mayFail1().pipe(
Either.mapLeft((e) => `aborted by an error from 1st function, ${e}`),
)) +
(yield* mayFail2().pipe(
Either.mapLeft((e) => `aborted by an error from 2nd function, ${e}`),
))
)
})
}注意:使用 Either.gen 时,你不需要用 Either.right 包装最终值。生成器的返回值会成为 Right。
你也可以用异步生成器函数配合 safeTry 来表示一个异步代码块。
在 Effect 这一侧,同样的模式改用 Effect.gen 而不是 Either.gen 来书写。
示例(用异步生成器处理多个失败)
import { ResultAsync, safeTry, ok } from "neverthrow"
declare function mayFail1(): ResultAsync<number, string>
declare function mayFail2(): ResultAsync<number, string>
function myFunc(): ResultAsync<number, string> {
return safeTry<number, string>(async function* () {
return ok(
(yield* mayFail1().mapErr(
(e) => `aborted by an error from 1st function, ${e}`,
)) +
(yield* mayFail2().mapErr(
(e) => `aborted by an error from 2nd function, ${e}`,
)),
)
})
}import { Effect } from "effect"
declare function mayFail1(): Effect.Effect<number, string>
declare function mayFail2(): Effect.Effect<number, string>
function myFunc(): Effect.Effect<number, string> {
return Effect.gen(function* () {
return (
(yield* mayFail1().pipe(
Effect.mapError((e) => `aborted by an error from 1st function, ${e}`),
)) +
(yield* mayFail2().pipe(
Effect.mapError((e) => `aborted by an error from 2nd function, ${e}`),
))
)
})
}注意:使用 Effect.gen 时,你不需要用 Effect.succeed 包装最终值。生成器的返回值会成为 Success。