neverthrow

repository·master·Indexed 25 days ago

https://github.com/supermacro/neverthrow

A library for functional error handling in TypeScript that replaces throwing errors with Result and ResultAsync types. It provides a Rust-like approach to encoding failure using Ok and Err variants, featuring utilities for transforming, chaining, and combining results, as well as an optional ESLint plugin to ensure results are explicitly handled.

Tokens
9.3K
Snippets
29
Records
57
Agent score
92%

What's inside neverthrow

  1. Install `eslint-plugin-neverthrow` for error handling safety

    master

    To ensure that Result values are not ignored and errors are explicitly handled, it is recommended to use eslint-plugin-neverthrow. This plugin forces you to consume a result by using one of the following methods:

    • .match
    • .unwrapOr
    • ._unsafeUnwrap

    This behavior mimics Rust's #[must_use] attribute.

    npm install eslint-plugin-neverthrow
    > npm install eslint-plugin-neverthrow
  2. Reduce boilerplate in error handling with safeTry

    master

    safeTry allows you to write complex logic involving multiple Result or ResultAsync returns using generator functions, avoiding repetitive manual error checks and unwrapping.

    Usage Pattern:

    1. Wrap your logic in a generator function or async generator function.
    2. Use yield* <RESULT> to indicate: 'If this is an Err, return it immediately from the safeTry block; otherwise, unwrap it to its value.'
    3. If using Promise<Result>, you must await the promise before using yield*.
    4. If using ResultAsync, you can use yield* directly.
    declare function mayFail1(): Promise<Result<number, string>>;
    declare function mayFail2(): ResultAsync<number, string>;
    
    function myFunc(): Promise<Result<number, string>> {
        return safeTry<number, string>(async function*() {
            return ok(
                (yield* (await mayFail1())
                    .mapErr(e => `aborted by an error from 1st function, ${e}`))
                +
                (yield* mayFail2()
                    .mapErr(e => `aborted by an error from 2nd function, ${e}`))
            )
        })
    }
  3. Use unsafe unwrap methods for testing

    master

    For testing purposes, Result instances provide two unsafe methods to extract values without manual pattern matching. Note: These should only be used in a test environment.

    • _unsafeUnwrap(): Returns the Ok value or throws a custom object if it is an Err.
    • _unsafeUnwrapErr(): Returns the Err value or throws a custom object if it is an Ok.

    By default, thrown errors do not include stack traces to keep Jest output clean. To include a stack trace, pass a configuration object.

    Tip: Since Result instances are comparable, you can often avoid unwrapping entirely by asserting equality against ok() or err() values.

    // Standard usage
    expect(myResult._unsafeUnwrap()).toBe(someExpectation)
    
    // Using with stack traces
    _unsafeUnwrapErr({
      withStackTrace: true,
    })
  4. Transform errors in ResultAsync with mapErr

    master

    Use mapErr to transform the contained Err value of a ResultAsync<T, E> into a new error type F. The transformation function can be synchronous or asynchronous. If the ResultAsync is an Ok, the value remains untouched. This is useful for converting low-level errors into more readable or domain-specific error messages.

    import { findUsersIn } from 'imaginary-database'
    // assume findUsersIn(country: string): ResultAsync<Array<User>, Error>
    
    const usersInCanada = findUsersIn("Canada").mapErr((error: Error) => {
      if(error.message === "Unknown country"){
        return error.message
      }
      return "System error, please contact an administrator."
    })
    
    // usersInCanada is of type ResultAsync<Array<User>, string>
    
    usersInCanada.then((usersResult: Result<Array<User>, string>) => {
      if(usersResult.isErr()){
        res.status(400).json({
          error: usersResult.error
        })
      }
      else{
        res.status(200).json({
          users: usersResult.value
        })
      }
    })
  5. Combine multiple Results with `Result.combine`

    master

    Use Result.combine to merge a list of Results into a single Result.

    • Behavior: It works like Promise.all. If all results are Ok, it returns an Ok containing a list (or tuple) of all values. If any result is an Err, it short-circuits and returns the first Err encountered.
    • Compatibility: Supports both homogeneous (same type) and heterogeneous (different types) lists.
    • Constraint: You cannot combine a list containing both Result and ResultAsync types.
    // Homogeneous list
    const resultList: Result<number, never>[] = [ok(1), ok(2)]
    const combinedList: Result<number[], unknown> = Result.combine(resultList)
    
    // Heterogeneous list (tuples)
    const tuple = <T extends any[]>(...args: T): T => args
    const resultTuple: [Result<string, never>, Result<string, never>] = tuple(ok('a'), ok('b'))
    const combinedTuple: Result<[string, string], unknown> = Result.combine(resultTuple)
  6. Chain asynchronous operations with andThen

    master

    Use andThen to sequence operations where the next step depends on the success of the previous one and might also fail. The callback must return a Result or ResultAsync. andThen flattens nested results, turning a ResultAsync<ResultAsync<A, E2>, E1> into a ResultAsync<A, E2>.

    import { validateUser } from 'imaginary-validator'
    import { insertUser } from 'imaginary-database'
    import { sendNotification } from 'imaginary-service'
    
    // assume signatures:
    // validateUser(user: User): Result<User, Error>
    // insertUser(user): ResultAsync<User, Error>
    // sendNotification(user): ResultAsync<void, Error>
    
    const resAsync = validateUser(user)
                   .andThen(insertUser)
                   .andThen(sendNotification)
    
    // resAsync is a ResultAsync<void, Error>
    
    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.")
      }
    })
  7. Wrap throwing functions with `Result.fromThrowable`

    master

    Use Result.fromThrowable to wrap synchronous functions that might throw exceptions. It returns a new function that, instead of throwing, returns an Err variant. Since the type of a thrown error is unknown, it is highly recommended to provide an errorFn as the second argument to map the error to a known type.

    import { Result } from 'neverthrow'
    
    type ParseError = { message: string }
    const toParseError = (): ParseError => ({ message: "Parse Error" })
    
    const safeJsonParse = Result.fromThrowable(JSON.parse, toParseError)
    
    // If JSON.parse throws, safeJsonParse returns an Err containing the ParseError
    const res = safeJsonParse("{")
  8. Combine multiple ResultAsync instances with all errors

    master

    Use ResultAsync.combineWithAllErrors to combine a list or tuple of ResultAsync instances without short-circuiting. Unlike combine, which returns only the first error encountered, combineWithAllErrors returns a ResultAsync containing a list of all error values from the failed results. If all results succeed, it returns the list of values. If some fail, the resulting error list contains only the errors from the failed items.

    const resultList: ResultAsync<number, string>[] = [
      okAsync(123),
      errAsync('boooom!'),
      okAsync(456),
      errAsync('ahhhhh!'),
    ]
    
    const result = ResultAsync.combineWithAllErrors(resultList)
    
    // result is Err(['boooom!', 'ahhhhh!'])
  9. Handle Result variants with match

    master
    The match method executes one of two provided functions based on whether the Result is Ok or Err. Unlike map/mapErr, both callbacks in match must return the same type A. This effectively unwraps the Result into type A.
  10. Perform async mapping with asyncMap

    master

    The asyncMap method is used when the transformation function returns a Promise. It returns a ResultAsync. If the original Result is an Err, the asyncMap callback is not executed.

    // parseHeaders returns Result<SomeKeyValueMap, ParseError>
    const asyncRes = parseHeaders(rawHeader)
      .map(headerKvMap => headerKvMap.Authorization)
      .asyncMap(findUserInDatabase)