Skip to content
Effect Days 2026 Get your ticket
Docs menu / Effect vs neverthrow

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() // true
result.isErr() // false

Effect

import * as Either from "effect/Either"
const result = Either.right({ myData: "test" })
Either.isRight(result) // true
Either.isLeft(result) // false

err

Example (Creating a failure result)

neverthrow

import { err } from "neverthrow"
const result = err("Oh no")
result.isOk() // false
result.isErr() // true

Effect

import * as Either from "effect/Either"
const result = Either.left("Oh no")
Either.isRight(result) // false
Either.isLeft(result) // true

map

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 it
const newResult = result.map((arr) => arr.map(parseInt))
newResult.isOk() // true

Effect

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 it
const newResult = result.pipe(Either.map((arr) => arr.map(parseInt)))
Either.isRight(newResult) // true

mapErr

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() // true
result.isErr() // false

Effect

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) // true
Either.isLeft(result) // false

errAsync

Example (Creating a failed async result)

neverthrow

import { errAsync } from "neverthrow"
const myResultAsync = errAsync("Oh no")
const myResult = await myResultAsync
myResult.isOk() // false
myResult.isErr() // true

Effect

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) // false
Either.isLeft(result) // true

fromThrowable

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 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(","))
}
})

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.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

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 = 10

Effect

import * as Effect from "effect/Effect"
const unwrapped = await Effect.runPromise(
Effect.fail(0).pipe(Effect.orElseSucceed(() => 10)),
)
// unwrapped = 10

andThen

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 fails
const 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 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

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 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}`,
)

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 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

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 Err
const 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 Either
const 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.