Skip to main content

Why Chain Operations?

Chaining allows you to compose multiple fallible operations without nested conditionals or try-catch blocks:
// Without chaining - nested and hard to read
const result1 = parseInput(input)
if (result1.isOk()) {
  const result2 = validate(result1.value)
  if (result2.isOk()) {
    const result3 = save(result2.value)
    if (result3.isOk()) {
      return result3.value
    }
  }
}

// With chaining - linear and readable
const result = parseInput(input)
  .andThen(validate)
  .andThen(save)

Using andThen for Sequential Operations

Basic andThen Pattern

andThen() is used when your callback returns another Result:
import { Result, ok, err } from 'neverthrow'

const square = (n: number): Result<number, string> => ok(n ** 2)

const result = ok(2)
  .andThen(square)  // Ok(4)
  .andThen(square)  // Ok(16)

result._unsafeUnwrap() // 16
If any step fails, the chain short-circuits:
const divide = (n: number): Result<number, string> => {
  if (n === 0) return err('Division by zero')
  return ok(10 / n)
}

const result = ok(2)
  .andThen(square)   // Ok(4)
  .andThen(divide)   // Err('Division by zero')
  .andThen(square)   // Skipped

result._unsafeUnwrapErr() // 'Division by zero'

Real-World Example: User Registration

1

Define validation functions

Each function returns a Result to handle validation errors:
import { Result, ok, err } from 'neverthrow'

interface RawUser {
  email: string
  password: string
}

interface ValidUser extends RawUser {
  id: string
}

function validateEmail(user: RawUser): Result<RawUser, string> {
  if (!user.email.includes('@')) {
    return err('Invalid email format')
  }
  return ok(user)
}

function validatePassword(user: RawUser): Result<RawUser, string> {
  if (user.password.length < 8) {
    return err('Password must be at least 8 characters')
  }
  return ok(user)
}

function checkEmailUnique(user: RawUser): Result<RawUser, string> {
  // Simulate database check
  const existingUsers = ['[email protected]']
  if (existingUsers.includes(user.email)) {
    return err('Email already registered')
  }
  return ok(user)
}

function createUser(user: RawUser): Result<ValidUser, string> {
  // Simulate user creation
  return ok({
    ...user,
    id: Math.random().toString(36)
  })
}
2

Chain the operations

Compose all validation and creation steps:
function registerUser(data: RawUser): Result<ValidUser, string> {
  return ok(data)
    .andThen(validateEmail)
    .andThen(validatePassword)
    .andThen(checkEmailUnique)
    .andThen(createUser)
}

// Usage
const result = registerUser({
  email: '[email protected]',
  password: 'secure123'
})

result.match(
  (user) => console.log('User created:', user.id),
  (error) => console.error('Registration failed:', error)
)
3

Handle the result

The chain stops at the first error, providing clear feedback:
// Valid input - all steps succeed
registerUser({
  email: '[email protected]',
  password: 'securepassword'
}) // Ok(ValidUser)

// Invalid email - stops at first validation
registerUser({
  email: 'invalid',
  password: 'securepassword'
}) // Err('Invalid email format')

// Weak password - stops at second validation
registerUser({
  email: '[email protected]',
  password: 'weak'
}) // Err('Password must be at least 8 characters')

Flattening Nested Results

andThen automatically flattens nested Results:
// Without andThen - nested Result
const nested = ok(ok(1234))
// Type: Ok<Ok<number, E>, E>

// With andThen - flattened
const flattened = nested.andThen(innerResult => innerResult)
// Type: Ok<number, E>
flattened._unsafeUnwrap() // 1234

map vs andThen

Understand when to use each method:
When your callback returns a regular value (not a Result):
const result = ok(5)
  .map(n => n * 2)        // Returns number
  .map(n => n.toString()) // Returns string

// Type: Result<string, E>

Error Context with mapErr

Add context to errors as they flow through the chain:
function parseJSON(input: string): Result<unknown, string> {
  try {
    return ok(JSON.parse(input))
  } catch {
    return err('JSON_PARSE_ERROR')
  }
}

interface Config {
  apiKey: string
  timeout: number
}

function validateConfig(data: unknown): Result<Config, string> {
  if (typeof data !== 'object' || data === null) {
    return err('CONFIG_INVALID_TYPE')
  }
  const obj = data as any
  if (!obj.apiKey || !obj.timeout) {
    return err('CONFIG_MISSING_FIELDS')
  }
  return ok(obj as Config)
}

// Chain with error context
function loadConfig(jsonString: string): Result<Config, string> {
  return parseJSON(jsonString)
    .mapErr(e => `Failed to parse JSON: ${e}`)
    .andThen(validateConfig)
    .mapErr(e => `Configuration error: ${e}`)
}

const result = loadConfig('invalid json')
result._unsafeUnwrapErr() // "Failed to parse JSON: JSON_PARSE_ERROR"

Distinct Error Types

Starting from v4.1.0, andThen supports returning different error types:
enum NetworkError {
  Timeout = 'TIMEOUT',
  NotFound = 'NOT_FOUND'
}

enum DatabaseError {
  ConnectionFailed = 'CONNECTION_FAILED',
  QueryFailed = 'QUERY_FAILED'
}

function fetchUser(id: string): Result<string, NetworkError> {
  // Simulate network request
  if (id === 'invalid') {
    return err(NetworkError.NotFound)
  }
  return ok(id)
}

function saveToCache(userId: string): Result<void, DatabaseError> {
  // Simulate database operation
  return ok(undefined)
}

// Error types are combined automatically
const result = fetchUser('123')
  .andThen(saveToCache)

// Type: Result<void, NetworkError | DatabaseError>

Advanced Pattern: Railway-Oriented Programming

Build complex pipelines that handle success and failure tracks:
interface Order {
  id: string
  items: string[]
  total: number
}

interface ProcessedOrder extends Order {
  confirmed: boolean
  trackingNumber?: string
}

function validateOrder(order: Order): Result<Order, string> {
  if (order.items.length === 0) {
    return err('Order must contain at least one item')
  }
  if (order.total <= 0) {
    return err('Order total must be positive')
  }
  return ok(order)
}

function applyDiscount(order: Order): Result<Order, string> {
  // Apply 10% discount for orders over $100
  const discounted = order.total > 100 
    ? { ...order, total: order.total * 0.9 }
    : order
  return ok(discounted)
}

function confirmOrder(order: Order): Result<ProcessedOrder, string> {
  return ok({
    ...order,
    confirmed: true,
    trackingNumber: 'TRK-' + Math.random().toString(36).slice(2, 9)
  })
}

function processOrder(order: Order): Result<ProcessedOrder, string> {
  return ok(order)
    .andThen(validateOrder)
    .andThen(applyDiscount)
    .map(order => ({ ...order, total: Math.round(order.total * 100) / 100 }))
    .andThen(confirmOrder)
}

// Usage
const order: Order = {
  id: 'ORD-001',
  items: ['item1', 'item2'],
  total: 150
}

processOrder(order).match(
  (processed) => {
    console.log('Order confirmed!')
    console.log('Final total:', processed.total) // 135 (with discount)
    console.log('Tracking:', processed.trackingNumber)
  },
  (error) => console.error('Order failed:', error)
)

Next Steps

Async Operations

Learn how to chain asynchronous operations with ResultAsync

Combining Results

Work with multiple Results at once using combine

Build docs developers (and LLMs) love