Skip to main content

Frequently Asked Questions

Find answers to common questions about NeverThrow, Result types, and error handling patterns.

General Questions

Although the package is called neverthrow, don’t take this literally. The name encourages developers to think more carefully about the ergonomics and usage of their software.Throwing and catching exceptions is very similar to using goto statements - it makes reasoning about programs harder. Additionally, by using throw you make the assumption that the caller of your function is implementing catch, which is a known source of errors.Example scenario: One developer throws an error, and another developer uses the function without prior knowledge that it will throw. An edge case has been left unhandled, leading to unhappy users.That said, there are definitely legitimate use cases for throwing in your program - just far fewer than you might think.
A Result type represents either success (Ok) or failure (Err). This makes it impossible to ignore errors because the error is explicitly part of the type signature.Instead of:
function divide(a: number, b: number): number {
  if (b === 0) throw new Error('Division by zero')
  return a / b
}
You write:
function divide(a: number, b: number): Result<number, string> {
  if (b === 0) return err('Division by zero')
  return ok(a / b)
}
The caller must handle the error case explicitly.
try/catch:
  • Errors are not visible in type signatures
  • Easy to forget to handle errors
  • Stack traces can be lost
  • Control flow is implicit
NeverThrow:
  • Errors are explicit in type signatures
  • Compiler forces error handling
  • Composable error handling with map, andThen, etc.
  • Control flow is explicit and easier to reason about
NeverThrow does not depend on any runtime-specific features. It works with:
  • Node.js (v18+)
  • Browsers (all modern browsers)
  • Deno
  • Bun
  • Any JavaScript runtime that supports ES6
The engines field in package.json specifies Node.js v18+ and npm v11+ primarily for maintaining a consistent development environment, not as a hard runtime requirement.
Yes! NeverThrow works with plain JavaScript. However, you lose the type safety benefits that make Result types powerful. The library will still provide the same API, but TypeScript’s compiler won’t help catch errors.For maximum benefit, we strongly recommend using TypeScript.

Working with Results

Use Result for synchronous operations:
function parseNumber(str: string): Result<number, string> {
  const num = Number(str)
  return isNaN(num) ? err('Invalid number') : ok(num)
}
Use ResultAsync for asynchronous operations:
function fetchUser(id: string): ResultAsync<User, Error> {
  return ResultAsync.fromPromise(
    fetch(`/api/users/${id}`).then(r => r.json()),
    (e) => new Error('Failed to fetch user')
  )
}
Note: ResultAsync is thenable and behaves like a native Promise, but with additional methods.
Use Result.fromThrowable or ResultAsync.fromThrowable:
import { Result } from 'neverthrow'

// Original throwing function
const originalFunction = (str: string) => JSON.parse(str)

// Convert to Result-returning function
type ParseError = { message: string }
const safeJsonParse = Result.fromThrowable(
  JSON.parse,
  (error): ParseError => ({ message: String(error) })
)

// Now safe to use
const result = safeJsonParse('{"name": "Alice"}')
// result is Result<any, ParseError>
map transforms the Ok value, returning a new Result:
ok(5)
  .map(x => x * 2)  // Returns Result<number, never>
  .map(x => x.toString())  // Returns Result<string, never>
andThen is for chaining operations that can fail:
ok(5)
  .andThen(x => x > 0 ? ok(x * 2) : err('Must be positive'))  
  // Returns Result<number, string>
  .andThen(x => ok(x.toString()))
  // Returns Result<string, string>
Use map when the transformation cannot fail, and andThen when it can.
Use Result.combine or Result.combineWithAllErrors:combine (short-circuits on first error):
const results = [
  ok(1),
  ok(2),
  ok(3)
]

const combined = Result.combine(results)
// Result<number[], never> containing [1, 2, 3]
combineWithAllErrors (collects all errors):
const results = [
  ok(1),
  err('error 1'),
  ok(3),
  err('error 2')
]

const combined = Result.combineWithAllErrors(results)
// Err(['error 1', 'error 2'])
safeTry reduces boilerplate when working with multiple Results in sequence. It uses generator functions to implicitly return early on errors:Without safeTry:
function process(): Result<number, string> {
  const result1 = step1()
  if (result1.isErr()) return result1
  
  const result2 = step2(result1.value)
  if (result2.isErr()) return result2
  
  return ok(result2.value * 2)
}
With safeTry:
function process(): ResultAsync<number, string> {
  return safeTry<number, string>(function*() {
    const value1 = yield* step1()
    const value2 = yield* step2(value1)
    return ok(value2 * 2)
  })
}
As of v8.1.0, you don’t need to call .safeUnwrap() anymore.

Advanced Patterns

Use andTee for Ok values or orTee for Err values:Logging success without changing the Result:
processUser(data)
  .andTee(user => logger.info('User processed', user))
  .andThen(saveToDatabase)
Logging errors without changing the Result:
processUser(data)
  .orTee(error => logger.error('Processing failed', error))
  .andThen(saveToDatabase)
Both methods let the original Result pass through regardless of the callback’s result.
andTee: Side effects, errors are ignored:
processUser(data)
  .andTee(logUser)  // If logUser fails, error is ignored
  .andThen(saveUser)
// Result type: Result<Saved, ProcessError | SaveError>
// Note: No LogError in the type
andThrough: Validation, errors propagate:
processUser(data)
  .andThrough(validateUser)  // If validation fails, error propagates
  .andThen(saveUser)
// Result type: Result<Saved, ProcessError | ValidationError | SaveError>
// Note: ValidationError IS in the type
Use asyncAndThen or asyncMap:
import { Result, ResultAsync } from 'neverthrow'

function validateSync(data: unknown): Result<Input, ValidationError> {
  // Synchronous validation
}

function saveAsync(input: Input): ResultAsync<Saved, DbError> {
  // Async database save
}

// Sync to async
const result: ResultAsync<Saved, ValidationError | DbError> = 
  validateSync(data).asyncAndThen(saveAsync)

// Or with asyncMap
const result2: ResultAsync<Data, ValidationError> = 
  validateSync(data).asyncMap(async (input) => {
    return await someAsyncOperation(input)
  })
Yes! Use ResultAsync.fromPromise:
import { ResultAsync } from 'neverthrow'

// Convert existing promise
const result = ResultAsync.fromPromise(
  fetch('/api/data').then(r => r.json()),
  (error) => ({ type: 'network', error })
)

// Chain with other Results
result
  .andThen(validateData)
  .andThen(processData)
  .match(
    (data) => console.log('Success:', data),
    (error) => console.error('Failed:', error)
  )

Testing

You have several options:Option 1: Use _unsafeUnwrap (recommended for tests):
import { expect, test } from 'vitest'

test('should parse valid input', () => {
  const result = parseInput('valid')
  expect(result._unsafeUnwrap()).toBe('parsed')
})
Option 2: Compare Results directly:
test('should return ok result', () => {
  const result = parseInput('valid')
  expect(result).toEqual(ok('parsed'))
})
Option 3: Check properties:
test('should fail on invalid input', () => {
  const result = parseInput('invalid')
  expect(result.isErr()).toBe(true)
  if (result.isErr()) {
    expect(result.error).toBe('Invalid input')
  }
})
It’s recommended to use a more lenient setting for test files since _unsafeUnwrap is legitimate in tests:
// eslint.config.js
export default [
  {
    files: ['src/**/*.ts'],
    rules: {
      'neverthrow/must-use-result': 'error'
    }
  },
  {
    files: ['**/*.test.ts', '**/*.spec.ts'],
    rules: {
      'neverthrow/must-use-result': 'warn'  // More lenient
    }
  }
]

Migration and Integration

Migrate incrementally:
  1. Start with new code: Write all new functions using Results
  2. Wrap throwing functions: Use Result.fromThrowable for existing functions
  3. Update critical paths: Convert error-prone code paths first
  4. Add ESLint plugin: Enforce Result handling in new code
  5. Gradually refactor: Convert remaining code over time
Example bridge function:
// Old throwing function
function oldFunction(): User {
  throw new Error('Not found')
}

// Bridge to Result
function newFunction(): Result<User, Error> {
  return Result.fromThrowable(oldFunction, (e) => e as Error)()
}
Yes! Results work great with web frameworks:
import { Request, Response } from 'express'
import { getUserById } from './services'

app.get('/users/:id', async (req: Request, res: Response) => {
  const result = await getUserById(req.params.id)
  
  result.match(
    (user) => res.json(user),
    (error) => {
      if (error.type === 'NotFound') {
        res.status(404).json({ error: 'User not found' })
      } else {
        res.status(500).json({ error: 'Internal error' })
      }
    }
  )
})
Results work well with UI frameworks:React example:
import { useState, useEffect } from 'react'
import { Result } from 'neverthrow'
import { fetchUser } from './api'

function UserProfile({ userId }: { userId: string }) {
  const [result, setResult] = useState<Result<User, Error> | null>(null)
  
  useEffect(() => {
    fetchUser(userId).then(setResult)
  }, [userId])
  
  if (!result) return <div>Loading...</div>
  
  return result.match(
    (user) => <div>Hello, {user.name}!</div>,
    (error) => <div>Error: {error.message}</div>
  )
}

Troubleshooting

Make sure you’ve installed the package:
npm install neverthrow
And that your TypeScript configuration includes the correct module resolution:
// tsconfig.json
{
  "compilerOptions": {
    "moduleResolution": "node",
    "esModuleInterop": true
  }
}
Most type issues can be resolved by:
  1. Updating TypeScript: NeverThrow requires TypeScript 4.7+
  2. Checking return types: Ensure functions explicitly return Result<T, E>
  3. Using explicit type annotations when needed:
// Explicit return type helps inference
function process(): Result<number, string> {
  return ok(42)
}

// Explicit error type when needed
const result: Result<number, string> = condition ? ok(1) : err('error')

Additional Resources

Build docs developers (and LLMs) love