Documentation Index
Fetch the complete documentation index at: https://mintlify.com/supermacro/neverthrow/llms.txt
Use this file to discover all available pages before exploring further.
Overview
The fromAsyncThrowable function wraps an async function that might throw exceptions and converts it into a function that returns a ResultAsync. This is the async equivalent of fromThrowable and is safer than using fromPromise with a function call.
fromAsyncThrowable is a top-level export that references ResultAsync.fromThrowable.
Type Signature
function fromAsyncThrowable<A extends readonly any[], R, E>(
fn: (...args: A) => Promise<R>,
errorFn?: (err: unknown) => E
): (...args: A) => ResultAsync<R, E>
Parameters
fn: The async function to wrap (returns a Promise that might reject)
errorFn: Optional function to map caught errors to a known type E
- If not provided, the raw error is used
- Maps both thrown errors (synchronous) and rejected promises (asynchronous)
Return Value
Returns a new function with the same parameters as the original, but returns a ResultAsync instead of a Promise that might reject.
Why Use fromAsyncThrowable?
Not all functions that return a Promise are truly async. Some can throw synchronously before returning the promise:
// DANGEROUS: Can throw synchronously!
function insertUser(user: User): Promise<User> {
if (!user.id) {
throw new TypeError('missing user id') // Synchronous throw!
}
return db.insert('users', user) // Async operation
}
// This will throw, NOT return a ResultAsync
const result = ResultAsync.fromPromise(
insertUser(myUser), // Throws before fromPromise can catch
() => new Error('Database error')
)
With fromAsyncThrowable:
import { fromAsyncThrowable } from 'neverthrow'
const safeInsertUser = fromAsyncThrowable(
insertUser, // Function reference, not call
() => new Error('Database error')
)
// Now it safely catches both sync throws and async rejections
const result = safeInsertUser(myUser)
// result is ResultAsync<User, Error>
Basic Usage
Wrapping Database Operations
import { fromAsyncThrowable } from 'neverthrow'
import { db } from './database'
type DbError = { type: 'DatabaseError'; message: string }
const safeInsert = fromAsyncThrowable(
db.insert,
(error): DbError => ({
type: 'DatabaseError',
message: error instanceof Error ? error.message : 'Unknown error'
})
)
const result = await safeInsert('users', { name: 'John', email: 'john@example.com' })
if (result.isOk()) {
console.log('Inserted:', result.value)
} else {
console.log('Error:', result.error.message)
}
Wrapping Fetch Requests
import { fromAsyncThrowable, ResultAsync } from 'neverthrow'
type FetchError =
| { type: 'NetworkError'; message: string }
| { type: 'InvalidResponse' }
const safeFetch = fromAsyncThrowable(
fetch,
(error): FetchError => ({
type: 'NetworkError',
message: error instanceof Error ? error.message : 'Request failed'
})
)
async function getUser(id: string): ResultAsync<User, FetchError> {
return safeFetch(`https://api.example.com/users/${id}`)
.andThen(response => {
if (!response.ok) {
return err({ type: 'InvalidResponse' })
}
return ResultAsync.fromPromise(
response.json(),
(): FetchError => ({ type: 'InvalidResponse' })
)
})
}
Real-World Examples
Email Service
import { fromAsyncThrowable } from 'neverthrow'
import nodemailer from 'nodemailer'
type EmailError =
| { type: 'InvalidConfig' }
| { type: 'SendFailed'; reason: string }
const transporter = nodemailer.createTransport(config)
const safeSendEmail = fromAsyncThrowable(
transporter.sendMail.bind(transporter),
(error): EmailError => ({
type: 'SendFailed',
reason: error instanceof Error ? error.message : 'Unknown error'
})
)
async function sendWelcomeEmail(user: User): ResultAsync<void, EmailError> {
return safeSendEmail({
from: 'noreply@example.com',
to: user.email,
subject: 'Welcome!',
text: `Welcome ${user.name}!`
}).map(() => undefined)
}
File Upload
import { fromAsyncThrowable } from 'neverthrow'
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'
type UploadError = { type: 'UploadFailed'; message: string }
const s3Client = new S3Client({ region: 'us-east-1' })
const safeUpload = fromAsyncThrowable(
async (key: string, body: Buffer) => {
const command = new PutObjectCommand({
Bucket: 'my-bucket',
Key: key,
Body: body
})
return await s3Client.send(command)
},
(error): UploadError => ({
type: 'UploadFailed',
message: error instanceof Error ? error.message : 'Upload failed'
})
)
const result = await safeUpload('uploads/image.png', imageBuffer)
External API with Retries
import { fromAsyncThrowable, ResultAsync, errAsync } from 'neverthrow'
type ApiError = 'NetworkError' | 'Timeout' | 'RateLimited'
const callApi = fromAsyncThrowable(
async (endpoint: string) => {
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), 5000)
try {
const response = await fetch(endpoint, { signal: controller.signal })
return await response.json()
} finally {
clearTimeout(timeout)
}
},
(error): ApiError => {
if (error instanceof Error) {
if (error.name === 'AbortError') return 'Timeout'
if (error.message.includes('rate limit')) return 'RateLimited'
}
return 'NetworkError'
}
)
async function callWithRetry(
endpoint: string,
maxRetries: number = 3
): ResultAsync<any, ApiError> {
let lastError: ApiError = 'NetworkError'
for (let i = 0; i < maxRetries; i++) {
const result = await callApi(endpoint)
if (result.isOk()) {
return result
}
lastError = result.error
// Don't retry on rate limit
if (result.error === 'RateLimited') {
break
}
// Wait before retry
await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)))
}
return errAsync(lastError)
}
Chaining Operations
import { fromAsyncThrowable, safeTry } from 'neverthrow'
const safeDbQuery = fromAsyncThrowable(
db.query,
() => 'QueryError' as const
)
const safeDbInsert = fromAsyncThrowable(
db.insert,
() => 'InsertError' as const
)
function processUser(userId: string) {
return safeTry(async function*() {
const user = yield* safeDbQuery('SELECT * FROM users WHERE id = ?', [userId])
const validated = yield* validateUser(user)
yield* safeDbInsert('audit_log', { action: 'user_processed', userId })
return ok(validated)
})
}
Comparison with fromPromise
Using fromPromise (Not Safe for Sync Throws)
import { ResultAsync } from 'neverthrow'
// NOT SAFE if fn can throw synchronously
function wrapper(user: User) {
return ResultAsync.fromPromise(
insertUser(user), // Might throw before fromPromise catches
() => 'Error'
)
}
Using fromAsyncThrowable (Safe)
import { fromAsyncThrowable } from 'neverthrow'
// SAFE: Catches both sync throws and async rejections
const safeInsertUser = fromAsyncThrowable(
insertUser,
() => 'Error'
)
function wrapper(user: User) {
return safeInsertUser(user) // Safe!
}
Key Points
- Wraps async functions that might throw
- Catches both synchronous throws and asynchronous rejections
- Safer than
fromPromise when wrapping function calls
- Returns a function that returns
ResultAsync
- Error mapper is optional but recommended for type safety
- Use this instead of
fromPromise when working with function references