Introduction to ResultAsync
ResultAsync wraps a Promise<Result<T, E>> and provides the same API as Result, allowing you to chain operations without awaiting at each step:
import { ResultAsync , okAsync , errAsync } from 'neverthrow'
// Create async results
const asyncOk = okAsync ( 42 ) // ResultAsync<number, never>
const asyncErr = errAsync ( 'oops' ) // ResultAsync<never, string>
// Await to get the Result
const result = await asyncOk
result . isOk () // true
result . _unsafeUnwrap () // 42
Creating ResultAsync
From Safe Promises
Use fromSafePromise for promises that never reject:
import { ResultAsync } from 'neverthrow'
const promise = Promise . resolve ( 42 )
const resultAsync = ResultAsync . fromSafePromise ( promise )
const result = await resultAsync
result . _unsafeUnwrap () // 42
From Promises with Error Handling
Use fromPromise to convert promises that might reject:
import { ResultAsync } from 'neverthrow'
interface User {
id : string
name : string
}
async function fetchUser ( id : string ) : Promise < User > {
const response = await fetch ( `/api/users/ ${ id } ` )
if ( ! response . ok ) {
throw new Error ( `HTTP ${ response . status } ` )
}
return response . json ()
}
// Convert to ResultAsync with error handler
const userResult = ResultAsync . fromPromise (
fetchUser ( '123' ),
( error ) => `Failed to fetch user: ${ error } `
)
// Type: ResultAsync<User, string>
const result = await userResult
result . match (
( user ) => console . log ( 'User:' , user . name ),
( error ) => console . error ( 'Error:' , error )
)
From Throwable Functions
Create a safe wrapper for functions that might throw:
import { ResultAsync } from 'neverthrow'
interface DatabaseError {
code : string
message : string
}
// Original async function that throws
async function insertUser ( user : User ) : Promise < User > {
// Might throw DatabaseError
return await db . insert ( 'users' , user )
}
// Create safe version
const safeInsertUser = ResultAsync . fromThrowable (
insertUser ,
( error ) : DatabaseError => ({
code: 'DB_ERROR' ,
message: String ( error )
})
)
// Now returns ResultAsync instead of throwing
const result = await safeInsertUser ({ id: '1' , name: 'Alice' })
// Type: Result<User, DatabaseError>
fromThrowable is safer than fromPromise because it catches both synchronous throws (before the promise is created) and asynchronous rejections.
Chaining Async Operations
Using map with Async Functions
Both sync and async functions work with map:
import { okAsync } from 'neverthrow'
const result = await okAsync ( 12 )
. map ( n => n * 2 ) // Sync function
. map ( n => Promise . resolve ( n + 10 )) // Async function
result . _unsafeUnwrap () // 34
Using andThen for Async Chains
andThen works with both Result and ResultAsync:
import { Result , ResultAsync , ok } from 'neverthrow'
function validateUser ( user : User ) : Result < User , string > {
if ( ! user . name ) return err ( 'Name required' )
return ok ( user )
}
function saveUser ( user : User ) : ResultAsync < User , string > {
return ResultAsync . fromPromise (
db . insert ( 'users' , user ),
( e ) => `Database error: ${ e } `
)
}
function sendWelcomeEmail ( user : User ) : ResultAsync < void , string > {
return ResultAsync . fromPromise (
emailService . send ( user . email , 'Welcome!' ),
( e ) => `Email error: ${ e } `
)
}
// Chain sync and async operations
const result = await validateUser ( user )
. andThen ( saveUser ) // Result -> ResultAsync
. andThen ( sendWelcomeEmail ) // ResultAsync -> ResultAsync
// Type: Result<void, string>
result . match (
() => console . log ( 'User created and welcomed!' ),
( error ) => console . error ( 'Failed:' , error )
)
Real-World Example: API Request Pipeline
Define the data types
interface ApiResponse {
data : unknown
status : number
}
interface User {
id : string
email : string
name : string
}
interface ValidationError {
field : string
message : string
}
Create helper functions
import { ResultAsync , err , ok } from 'neverthrow'
function fetchApi ( url : string ) : ResultAsync < ApiResponse , string > {
return ResultAsync . fromPromise (
fetch ( url ). then ( r => ({
data: r . json (),
status: r . status
})),
( error ) => `Network error: ${ error } `
)
}
function checkStatus ( response : ApiResponse ) : Result < ApiResponse , string > {
if ( response . status >= 400 ) {
return err ( `HTTP ${ response . status } ` )
}
return ok ( response )
}
function parseUser ( response : ApiResponse ) : Result < User , ValidationError > {
const data = response . data as any
if ( ! data . id || ! data . email || ! data . name ) {
return err ({
field: 'user' ,
message: 'Missing required fields'
})
}
return ok ({
id: data . id ,
email: data . email ,
name: data . name
})
}
function cacheUser ( user : User ) : ResultAsync < User , string > {
return ResultAsync . fromPromise (
cache . set ( `user: ${ user . id } ` , user ),
( e ) => `Cache error: ${ e } `
). map (() => user ) // Return original user
}
Compose the pipeline
function getUserById (
id : string
) : ResultAsync < User , string | ValidationError > {
return fetchApi ( `/api/users/ ${ id } ` )
. andThen ( checkStatus )
. andThen ( parseUser )
. andThen ( cacheUser )
}
// Usage
const result = await getUserById ( '123' )
result . match (
( user ) => console . log ( 'Fetched user:' , user . name ),
( error ) => {
if ( typeof error === 'string' ) {
console . error ( 'System error:' , error )
} else {
console . error ( `Validation error on ${ error . field } : ${ error . message } ` )
}
}
)
Async Error Mapping
mapErr also supports async functions:
import { errAsync } from 'neverthrow'
async function logError ( error : string ) : Promise < string > {
await logger . error ( error )
return `Logged: ${ error } `
}
const result = await errAsync ( 'Database connection failed' )
. mapErr ( logError ) // Async error transformation
result . _unsafeUnwrapErr () // "Logged: Database connection failed"
Combining Sync and Async Results
From Result to ResultAsync
Use asyncAndThen or asyncMap on a regular Result:
import { ok } from 'neverthrow'
// asyncAndThen: Result -> ResultAsync
const result1 = ok ( 42 )
. asyncAndThen ( n => okAsync ( n * 2 ))
// Type: ResultAsync<number, never>
// asyncMap: Result -> ResultAsync
const result2 = ok ( 42 )
. asyncMap ( n => Promise . resolve ( n * 2 ))
// Type: ResultAsync<number, never>
From ResultAsync to Result
Simply await the ResultAsync:
const asyncResult : ResultAsync < number , string > = okAsync ( 42 )
const syncResult : Result < number , string > = await asyncResult
Using with async/await
ResultAsync is thenable, so it works seamlessly with async/await:
import { okAsync , errAsync } from 'neverthrow'
async function processData () {
// Can await directly
const result = await okAsync ( 42 )
. map ( n => n * 2 )
if ( result . isOk ()) {
console . log ( result . value ) // 84
}
}
// Works with Promise.all
const results = await Promise . all ([
okAsync ( 1 ),
okAsync ( 2 ),
okAsync ( 3 )
])
// results is Result<number, E>[]
results . forEach ( r => {
if ( r . isOk ()) console . log ( r . value )
})
Error Recovery with orElse
Recover from errors asynchronously:
import { ResultAsync , okAsync , errAsync } from 'neverthrow'
function fetchFromPrimary ( id : string ) : ResultAsync < string , string > {
return errAsync ( 'Primary database down' )
}
function fetchFromBackup ( error : string ) : ResultAsync < string , string > {
console . log ( 'Falling back due to:' , error )
return okAsync ( 'Data from backup' )
}
const result = await fetchFromPrimary ( '123' )
. orElse ( fetchFromBackup )
result . _unsafeUnwrap () // "Data from backup"
Side Effects with andTee
Perform side effects without affecting the result:
import { okAsync } from 'neverthrow'
const result = await okAsync ({ id: '1' , name: 'Alice' })
. andTee ( user => {
// Side effect: logging
console . log ( 'Processing user:' , user . name )
})
. andTee ( user => {
// Side effect: analytics
analytics . track ( 'user_processed' , { userId: user . id })
})
. map ( user => user . name )
// Result still contains the user name
result . _unsafeUnwrap () // "Alice"
Even if the side effect throws, the result is unaffected:
const result = await okAsync ( 42 )
. andTee (() => {
throw new Error ( 'Logging failed' )
})
. map ( n => n * 2 )
result . _unsafeUnwrap () // 84 - still succeeds
Pattern: Parallel Async Operations
Use combine for multiple async operations (covered in detail in Combining Results ):
import { ResultAsync } from 'neverthrow'
const operations = [
fetchUser ( '1' ),
fetchUser ( '2' ),
fetchUser ( '3' )
]
// All succeed or fail together
const combined = await ResultAsync . combine ( operations )
combined . match (
( users ) => console . log ( `Fetched ${ users . length } users` ),
( error ) => console . error ( 'At least one fetch failed:' , error )
)
Converting Throwable Code
Wrap existing promise-based code:
Before (throws)
After (safe)
async function saveOrder ( order : Order ) : Promise < Order > {
const validated = await validateOrder ( order )
const saved = await db . insert ( 'orders' , validated )
await sendConfirmation ( saved . id )
return saved
}
// Usage requires try-catch
try {
const order = await saveOrder ( myOrder )
console . log ( 'Success:' , order . id )
} catch ( error ) {
console . error ( 'Failed:' , error )
}
function saveOrder ( order : Order ) : ResultAsync < Order , string > {
return ResultAsync . fromPromise (
validateOrder ( order ),
( e ) => `Validation failed: ${ e } `
)
. andThen ( validated =>
ResultAsync . fromPromise (
db . insert ( 'orders' , validated ),
( e ) => `Database error: ${ e } `
)
)
. andThen ( saved =>
ResultAsync . fromPromise (
sendConfirmation ( saved . id ),
( e ) => `Email error: ${ e } `
). map (() => saved ) // Return the order
)
}
// Usage - no try-catch needed
const result = await saveOrder ( myOrder )
result . match (
( order ) => console . log ( 'Success:' , order . id ),
( error ) => console . error ( 'Failed:' , error )
)
Next Steps
Combining Results Learn how to work with multiple Results using combine
Error Recovery Master error handling with orElse, match, and unwrapOr