Core API

safe.async

Executes an asynchronous function and returns a Promise<SafeResult> tuple. Supports automatic retry with configurable backoff.


Signatures

safe.async<T>(fn: (signal?: AbortSignal) => Promise<T>): Promise<SafeResult<T, Error>>
safe.async<T>(fn: (signal?: AbortSignal) => Promise<T>, hooks: SafeAsyncHooks<T, Error, []>): Promise<SafeResult<T, Error>>
safe.async<T, E>(fn: (signal?: AbortSignal) => Promise<T>, parseError: (e: unknown) => NonFalsy<E>): Promise<SafeResult<T, E>>
safe.async<T, E>(fn: (signal?: AbortSignal) => Promise<T>, parseError: (e: unknown) => NonFalsy<E>, hooks: SafeAsyncHooks<T, E, []> & { defaultError: E }): Promise<SafeResult<T, E>>

When abortAfter is configured, the function receives an AbortSignal as its first parameter for cooperative cancellation.

Error normalization

When no parseError is provided, non-Error thrown values (strings, numbers, etc.) are automatically normalized to Error instances via new Error(String(e)). The original thrown value is preserved as error.cause.


Basic usage

const [user, error] = await safe.async(() => fetchUser(userId))

if (error) {
  console.error('Fetch failed:', error.message)
  return
}
console.log(user) // user is typed

With error mapping

type ApiError = { type: string; statusCode: number; message: string }

const [data, error] = await safe.async(
  () =>
    fetch('/api/data').then((r) => {
      if (!r.ok) throw new Error(`HTTP ${r.status}`)
      return r.json()
    }),
  (e): ApiError => ({
    type: 'NETWORK_ERROR',
    statusCode: 500,
    message: e instanceof Error ? e.message : 'Unknown error',
  })
)

if (error) {
  console.error(error.type, error.statusCode) // error is typed as ApiError
}

With hooks

const [user, error] = await safe.async(() => fetchUser(userId), {
  onSuccess: (user, []) => console.log('Fetched user:', user.name),
  onError: (error, []) => reportToSentry(error),
})

With error mapping and hooks

const [result, error] = await safe.async(
  () => processPayment(amount),
  (e): PaymentError => ({ code: 'PAYMENT_FAILED', message: String(e) }),
  {
    onSuccess: (receipt, []) => {
      analytics.track('payment_success', { amount: receipt.amount })
    },
    onError: (error, []) => {
      analytics.track('payment_failed', { code: error.code })
      alertOpsTeam(error)
    },
  }
)

With retry

safe.async supports automatic retry via the retry option in hooks. See Retry support for full details.

const [data, error] = await safe.async(() => fetchWithRetry('/api/data'), {
  retry: { times: 3, waitBefore: (attempt) => attempt * 1000 },
  onRetry: (error, attempt, []) => {
    console.log(`Attempt ${attempt} failed, retrying...`)
  },
})

With timeout

safe.async supports automatic timeout via the abortAfter option. See Timeout / Abort for full details.

const [data, error] = await safe.async(
  (signal) => fetch('/api/data', { signal }),
  { abortAfter: 5000 }
)

When abortAfter is configured, an AbortSignal is passed as the first parameter to your function.

Previous
safe.sync