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)
neverthrow
import { ok } from "neverthrow"
const result = ok({ myData: "test" })
result.isOk() // trueresult.isErr() // falseEffect
import * as Either from "effect/Either"
const result = Either.right({ myData: "test" })
Either.isRight(result) // trueEither.isLeft(result) // falseerr
Example (Creating a failure result)
neverthrow
import { err } from "neverthrow"
const result = err("Oh no")
result.isOk() // falseresult.isErr() // trueEffect
import * as Either from "effect/Either"
const result = Either.left("Oh no")
Either.isRight(result) // falseEither.isLeft(result) // truemap
Example (Transforming the success value)
neverthrow
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() // trueEffect
import * 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)
neverthrow
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))Effect
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)
neverthrow
import { err } from "neverthrow"
const result = err("Oh no")
const multiply = (value: number): number => value * 2
const unwrapped = result.map(multiply).unwrapOr(10)Effect
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)
neverthrow
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)Effect
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)
neverthrow
import { ok, okAsync } from "neverthrow"
// const result: ResultAsync<number, never>const result = ok(1).asyncAndThen((n) => okAsync(n + 1))Effect
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)
neverthrow
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),)Effect
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)
neverthrow
import { Result } from "neverthrow"
declare const myResult: Result<number, string>
myResult.match( (value) => `The value is ${value}`, (error) => `The error is ${error}`,)Effect
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)
neverthrow
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), )Effect
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)
neverthrow
import { Result, ok } from "neverthrow"
const results: Result<number, string>[] = [ok(1), ok(2)]
// const combined: Result<number[], string>const combined = Result.combine(results)Effect
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)
neverthrow
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!'])Effect
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)
neverthrow
import { okAsync } from "neverthrow"
const myResultAsync = okAsync({ myData: "test" })
const result = await myResultAsync
result.isOk() // trueresult.isErr() // falseEffect
import * 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)
neverthrow
import { errAsync } from "neverthrow"
const myResultAsync = errAsync("Oh no")
const myResult = await myResultAsync
myResult.isOk() // falsemyResult.isErr() // trueEffect
import * 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)
neverthrow
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"),)Effect
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)
neverthrow
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(",")) }})Effect
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)
neverthrow
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(",")) }})Effect
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)
neverthrow
import { errAsync } from "neverthrow"
const unwrapped = await errAsync(0).unwrapOr(10)// unwrapped = 10Effect
import * as Effect from "effect/Effect"
const unwrapped = await Effect.runPromise( Effect.fail(0).pipe(Effect.orElseSucceed(() => 10)),)// unwrapped = 10andThen
Example (Chaining multiple async computations)
neverthrow
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.") }})Effect
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)
neverthrow
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) }})Effect
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)
neverthrow
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}`, )Effect
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)
neverthrow
import { ResultAsync, okAsync } from "neverthrow"
const resultList: ResultAsync<number, string>[] = [okAsync(1), okAsync(2)]
// const combinedList: ResultAsync<number[], string>const combinedList = ResultAsync.combine(resultList)Effect
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)
neverthrow
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!'])Effect
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)
neverthrow
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("{")Effect
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)
neverthrow
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}`, )), ) })}Effect
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)
neverthrow
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}`, )), ) })}Effect
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.