Effect vs neverthrow
When working with error handling in TypeScript, both neverthrow and Effect provide useful abstractions for modeling
success and failure without exceptions. They share many concepts, such as wrapping computations in a safe container,
transforming values with map, handling errors with mapErr/mapLeft, and offering utilities to combine or unwrap results.
This page shows a side-by-side comparison of neverthrow and Effect APIs for common use cases. If you’re already familiar with neverthrow, the examples will help you understand how to achieve the same patterns with Effect. If you’re starting fresh, the comparison highlights the similarities and differences so you can decide which library better fits your project.
neverthrow exposes instance methods (for example, result.map(...)).
Effect exposes functions on Either (for example, Either.map(result, ...)) and supports a pipe style for readability and better tree shaking.
Synchronous API
ok
Example (Creating a success result)
import { ok } from "neverthrow"
const result = ok({ myData: "test" })
result.isOk() // trueresult.isErr() // falseimport * as Either from "effect/Either"
const result = Either.right({ myData: "test" })
Either.isRight(result) // trueEither.isLeft(result) // falseerr
Example (Creating a failure result)
import { err } from "neverthrow"
const result = err("Oh no")
result.isOk() // falseresult.isErr() // trueimport * as Either from "effect/Either"
const result = Either.left("Oh no")
Either.isRight(result) // falseEither.isLeft(result) // truemap
Example (Transforming the success value)
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 itconst 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 itconst newResult = result.pipe(Either.map((arr) => arr.map(parseInt)))
Either.isRight(newResult) // truemapErr
Example (Transforming the error value)
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
Example (Providing a default value)
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
Example (Chaining computations that may fail)
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
Example (Chaining asynchronous computations that may fail)
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
Example (Providing an alternative on failure)
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
Example (Pattern matching on success or failure)
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
Example (Parsing headers and looking up a user)
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), ),)Note. In neverthrow, asyncMap works with Promises directly.
In Effect, passing a Promise to combinators like Effect.andThen automatically lifts it into an Effect.
If the Promise rejects, the rejection is turned into an UnknownException, which is why the error type is widened to string | UnknownException.
combine
Example (Combining multiple results)
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
Example (Collecting all errors and successes)
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]Note. There is no exact equivalent of Result.combineWithAllErrors in Effect.
Use Array.getLefts to collect all errors and Array.getRights to collect all successes.
Asynchronous API
In the examples below we use Effect.runPromise to run an effect and return a Promise.
You can also use other APIs such as Effect.runPromiseExit, which can capture additional cases like defects (runtime errors) and interruptions.
okAsync
Example (Creating a successful async result)
import { okAsync } from "neverthrow"
const myResultAsync = okAsync({ myData: "test" })
const result = await myResultAsync
result.isOk() // trueresult.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) // trueEither.isLeft(result) // falseerrAsync
Example (Creating a failed async result)
import { errAsync } from "neverthrow"
const myResultAsync = errAsync("Oh no")
const myResult = await myResultAsync
myResult.isOk() // falsemyResult.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) // falseEither.isLeft(result) // truefromThrowable
Example (Wrapping a Promise-returning function that may throw)
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
Example (Transforming the success value)
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 awaitnamesInCanada.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.eitherEffect.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
Example (Transforming the error value)
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
Example (Providing a default value when async fails)
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
Example (Chaining multiple async computations)
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
Example (Fallback when an async operation fails)
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 failsconst 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 failsconst 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
Example (Handling success and failure at the end of a chain)
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 matchconst 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 matchconst 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
Example (Combining multiple async results)
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
Example (Collecting all errors instead of failing fast)
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!'])Utilities
fromThrowable
Example (Safely wrapping a throwing function)
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 Errconst 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 Eitherconst result = safeJsonParse("{")safeTry
Example (Using generators to simplify error handling)
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}`))) ) })}Note. With Either.gen, you do not need to wrap the final value with Either.right. The generator’s return value becomes the Right.
You can also use an async generator function with safeTry to represent an asynchronous block.
On the Effect side, the same pattern is written with Effect.gen instead of Either.gen.
Example (Using async generators to handle multiple failures)
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}`), )) ) })}Note. With Effect.gen, you do not need to wrap the final value with Effect.succeed. The generator’s return value becomes the Success.