Execution Orchestration Layer for JavaScript/TypeScript.
Replace imperative try/catch/finally blocks with declarative async function wrappers. Airbag handles loading states, error catching, retries with exponential backoff, timeouts, and circuit breaking — fully type-safe with zero runtime dependencies.
npm install airbagimport { airbag } from 'airbag';
interface User {
id: string;
name: string;
}
const fetchUser = async (id: string): Promise<User> => {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error('Failed to fetch user');
return res.json() as Promise<User>;
};
const safeFetchUser = airbag(fetchUser, {
timeout: 5000,
retries: 3,
onLoading: (loading) => spinner.toggle(loading),
onSuccess: (user) => toast.success(`Loaded ${user.name}`),
onError: (err) => toast.error(err.message),
});
// Fully type-safe — same signature as fetchUser
const user = await safeFetchUser('user-123');Three levels of configuration merged in order of specificity:
import { createAirbagInstance } from 'airbag';
// Level 1: Global defaults
const { wrap } = createAirbagInstance({
retries: 3,
timeout: 10000,
onError: (err) => logger.error(err),
});
// Level 2: Instance options (merged over global)
const safeFetch = wrap(fetchUser, {
name: 'fetchUser',
timeout: 5000,
});
// Level 3: Execution overrides (merged over instance)
const user = await safeFetch.with({ timeout: 15000 })('user-123');Wraps an async function with execution orchestration.
const wrapped = airbag(myAsyncFn, { timeout: 5000, retries: 3 });
const result = await wrapped(...originalArgs);Creates a factory with shared global defaults.
const { wrap, configure, getDefaults } = createAirbagInstance({ retries: 2 });Creates a new wrapped function with execution-level overrides while sharing the same circuit breaker state.
const urgent = wrapped.with({ timeout: 2000 });
const result = await urgent(...args);Resets the circuit breaker state back to closed.
| Option | Type | Default | Description |
|---|---|---|---|
name |
string |
'anonymous' |
Identifier for logging and error messages |
timeout |
number |
30000 |
Timeout in ms (0 disables) |
retries |
number |
0 |
Shorthand for retry.count |
retry.count |
number |
0 |
Number of retries after initial failure |
retry.backoff |
'exponential' | 'linear' | 'fixed' |
'exponential' |
Backoff strategy between retries |
retry.baseDelay |
number |
1000 |
Base delay in ms |
retry.maxDelay |
number |
30000 |
Maximum delay cap in ms |
retry.jitter |
boolean |
true |
Randomize delays to avoid thundering herd |
circuitBreaker.enabled |
boolean |
false |
Enable the circuit breaker |
circuitBreaker.threshold |
number |
5 |
Consecutive failures before opening |
circuitBreaker.resetTimeout |
number |
60000 |
Ms before probing with a half-open attempt |
signal |
AbortSignal |
— | Cancel execution via AbortController |
Lifecycle callbacks can be passed flat at the top level — no nesting required:
airbag(fetchUser, {
retries: 3,
onSuccess: (user) => toast.success(user.name),
onError: (err) => toast.error(err.message),
});For reusable callback sets, group them in an adapter object instead:
const logger: AirbagAdapter = {
onError: (err, ctx) => log.error(ctx.functionName, err),
onFinish: (ctx) => log.info(`Done in ${ctx.duration}ms`),
};
airbag(fetchUser, { retries: 3, adapter: logger });If both flat callbacks and an adapter are provided, the adapter takes precedence.
Every callback receives an ExecutionContext:
interface ExecutionContext {
functionName: string;
duration: number;
timestamp: number;
attempt: number;
maxAttempts: number;
}All errors extend AirbagError with a code and context property:
| Error | Code | When |
|---|---|---|
TimeoutError |
TIMEOUT |
Promise exceeded the configured timeout |
RetryExhaustedError |
RETRY_EXHAUSTED |
All retry attempts failed |
CircuitOpenError |
CIRCUIT_OPEN |
Circuit breaker blocked execution |
AbortError |
ABORTED |
Execution cancelled via AbortSignal |
import { TimeoutError, RetryExhaustedError } from 'airbag';
const result = await safeFetch('id').catch((err) => {
if (err instanceof TimeoutError) {
// err.timeout — the configured timeout in ms
// err.context — execution metadata
}
if (err instanceof RetryExhaustedError) {
// err.cause — the last error that caused the final failure
}
});MIT