Getting started
In this tutorial, we build a policy around a slow function. We give each attempt a timeout, retry the attempts that time out, add a fallback value, and then read the outcome as a typed Result.
You need Node.js 22.18 or later, which runs TypeScript files directly.
-
Create a project and install Tenace.
Terminal window mkdir tenace-tutorial && cd tenace-tutorialpnpm init && pnpm pkg set type=modulepnpm add @julr/tenaceWith npm, run
npm init -y,npm pkg set type=module, andnpm install @julr/tenaceinstead. -
Create
profile.tswith a function that hangs on its first two attempts and succeeds on the third. It stops when itssignalaborts, likefetchdoes.profile.ts import { type Context, Tenace } from '@julr/tenace'async function fetchProfile({ signal, attempt }: Context): Promise<{ name: string }> {console.log(`attempt ${attempt}`)if (attempt < 3) {await new Promise((_resolve, reject) => {signal.addEventListener('abort', () => reject(signal.reason), { once: true })})}return { name: 'Ada' }}const policy = Tenace.policy().withTimeout('100ms')try {await policy.execute(fetchProfile)} catch (error) {console.log(String(error))}Run it:
Terminal window node profile.tsThe first attempt hangs, so the timeout aborts it after 100 ms and the policy rejects:
attempt 1TimeoutError: Operation timed out after 100ms -
Retry the attempts that time out. Replace the policy and the
tryblock with the following code:profile.ts import { backoff, Tenace } from '@julr/tenace'const policy = Tenace.policy().withTimeout('100ms').withRetry({retries: 3,delay: backoff.exponential({ base: '50ms' }),retryOn: ['TimeoutError'],})console.log(await policy.execute(fetchProfile))The timeout comes first, so it bounds each attempt. The retry wraps it and starts a new attempt after each
TimeoutError:attempt 1attempt 2attempt 3{ name: 'Ada' } -
Add a fallback for the case where every attempt times out. Lower
retriesto1to see it:profile.ts import { Tenace } from '@julr/tenace'const policy = Tenace.policy().withTimeout('100ms').withRetry({ retries: 1, delay: '50ms', retryOn: ['TimeoutError'] }).withFallbackOn(['TimeoutError'], () => ({ name: 'Guest' }))console.log(await policy.execute(fetchProfile))console.log(policy.describe())Two attempts time out, then the fallback returns its value.
describe()lists the strategies from the outermost to the innermost:attempt 1attempt 2{ name: 'Guest' }[ 'fallback', 'retry', 'timeout' ] -
Read the outcome as a
Resultinstead of a promise that may reject. Remove the fallback, callsettle(), and handle each case withmatch():profile.ts import { match, Tenace } from '@julr/tenace'const policy = Tenace.policy().withTimeout('100ms').withRetry({ retries: 1, delay: '50ms', retryOn: ['TimeoutError'] })const result = await policy.settle(fetchProfile)console.log(match(result, {ok: profile => `Hello ${profile.name}`,TimeoutError: error => `Timed out after ${error.timeoutMs}ms`,UnexpectedError: error => `Failed: ${String(error.cause)}`,}),)attempt 1attempt 2Timed out after 100msmatch()requires one handler per error the policy can produce. Remove theTimeoutErrorhandler and TypeScript reports the missing case.
We built a policy that bounds each attempt, retries timeouts with exponential backoff, falls back to a default value, and reports its errors in the type of its Result.
Next steps
Section titled “Next steps”- Strategy order explains why the timeout came before the retry.
execute()andsettle()compares the two ways to run a policy.- Typed errors shows how your own errors join the error channel.