Deadline-based timeouts for async code in Node.js. Enforce end-to-end execution deadlines with automatic propagation and AbortSignal support.
npm install safe-timeoutsDeadline-based timeouts for async Node.js code with AbortSignal support.
!NPM Version
!NPM Downloads
!GitHub package.json version
!GitHub last commit
!GitHub contributors
!GitHub forks
!GitHub Repo stars
!GitHub License
Promise-based deadline enforcement for async code in Node.js. safe-timeouts helps you apply a single execution deadline across async functions, services, and external calls using standard AbortSignal semantics.
---
In real backend systems, timeouts are end-to-end, not per-function:
* An HTTP request has a deadline
* That deadline must apply across DB calls, service logic, and external APIs
* Nested functions should not accidentally extend the available time
Most timeout utilities fail here because they:
* don’t propagate context
* don’t compose across nested calls
* don’t integrate with AbortSignal
safe-timeouts solves this correctly.
---
``bash`
npm install safe-timeouts
Node.js >= 16 is required.
---
`ts
import { withTimeout, TimeoutError, safeAxios } from "safe-timeouts";
import axios from "axios";
try {
const resultWithSafeAxios = await withTimeout(2000, async () => {
const res = await safeAxios.get("https://api.example.com/users"); // no signal to be passed.
return res.data;
});
const resultWithAxios = await withTimeout(2000, async (signal) => { // signal to be taken.
const res = await axios.get("https://api.example.com/users", {signal}); // signal to be passed.
return res.data;
});
} catch (err) {
if (err instanceof TimeoutError) {
console.error("Request timed out");
}
}
`
What happens:
* A 2s deadline is created
* An AbortController is started internally
* If the deadline is exceeded:
* the promise rejects with TimeoutErrorAbortSignal
* the is aborted
* Axios cancels the HTTP request
---
Deadlines propagate and compose automatically.
`ts
await withTimeout(3000, async () => {
await serviceA(); // uses part of the budget
await withTimeout(5000, async () => {
await serviceB(); // still limited by the original 3s
});
});
`
The inner timeout cannot extend the outer deadline.
This makes time budgets safe and deterministic.
---
safeAxios is a convenience wrapper around Axios that automatically integrates with safe-timeouts.
When used inside withTimeout, HTTP requests are automatically cancellable.
When used outside withTimeout, it behaves exactly like a normal Axios instance.
Example
`ts
import { withTimeout, safeAxios } from "safe-timeouts";
await withTimeout(2000, async () => {
const res = await safeAxios.get("/users");
return res.data;
});
`
ts
import { withTimeout, createSafeAxios } from "safe-timeouts";const api = createSafeAxios({
baseURL: "https://api.example.com",
});
await withTimeout(1000, async () => {
await api.post("/sync");
});
`How it works (context propagation)
safe-timeouts uses AsyncLocalStorage to propagate timeout context across async boundaries.
Example flow
`ts
await withTimeout(2000, async () => {
await controller();
});async function controller() {
return serviceA();
}
async function serviceA() {
return serviceB();
}
async function serviceB() {
return safeAxios.get("/users");
}
Context flow diagram
withTimeout
└─ Async context (deadline + AbortController)
├─ controller()
│ └─ serviceA()
│ └─ serviceB()
│ └─ safeAxios.get()
│ └─ axios(request + signal)
`
The timeout context is created once
Node automatically propagates it across async calls
safeAxios reads the context at request time When the deadline expires, the request is aborted---
Using with services (multiple layers) without safeAxios
`ts
import axios from "axios";await withTimeout(2000, async (signal) => {
await controller(signal);
});
async function controller(signal) {
await serviceA(signal);
}
async function serviceA(signal) {
await serviceB(signal);
}
async function serviceB(signal) {
const res = await axios.get("/users", { signal });
return res.data;
}
`All functions share the same deadline by passing the same
AbortSignal down the call chain.---
Abort-aware vs non-abort-aware operations
$3
These stop execution as soon as the deadline is exceeded:
*
fetch (Node 18+)
* axios (with { signal })
* fs/promises (partial)
* stream.pipeline
* timers/promisesExample:
`ts
// GET
await safeAxios.get(url); // 👈 No AbortSignal needed
// POST
await safeAxios.post(
url,
{ name: "Aryan", role: "admin" },
{
// 👈 No AbortSignal goes here
headers: {
"Content-Type": "application/json",
Authorization: "Bearer YOUR_TOKEN",
},
}) // GET
await axios.get(url, { signal }); // 👈 AbortSignal goes here
// POST
await axios.post(
url,
{ name: "Aryan", role: "admin" },
{
signal, // 👈 AbortSignal goes here
headers: {
"Content-Type": "application/json",
Authorization: "Bearer YOUR_TOKEN",
},
})
`$3
These cannot be forcibly stopped:
*
setTimeout / sleep
* Sequelize queries
* CPU-bound loops
* legacy librariesFor these,
safe-timeouts:* stops waiting
* rejects the outer promise
* allows you to guard further logic
---
Non-abort-aware operations and control flow
JavaScript cannot forcibly stop non-abort-aware operations (like
setTimeout, Sequelize queries, or CPU-bound work).When such operations exceed the deadline:
*
safe-timeouts rejects the outer promise
* abort-aware APIs are cancelled automatically
* JavaScript execution resumes only when the pending operation completesTo keep control flow predictable:
* prefer calling abort-aware APIs (Axios, fetch, streams) after non-abort-aware work
* abort-aware APIs will throw immediately if the deadline has already been exceeded
This design avoids hidden global checks while remaining honest about JavaScript limitations.
---
Axios integration
safe-timeouts works with Axios by passing the provided AbortSignal to the request.`ts
import axios from "axios";
import { withTimeout } from "safe-timeouts";await withTimeout(2000, async (signal) => {
const res = await axios.get("/users", { signal });
return res.data;
});
`Axios is abort-aware:
* if the deadline is exceeded before the request starts, Axios throws immediately
* if the deadline is exceeded while the request is in flight, Axios cancels the request
This explicit integration keeps cancellation predictable and avoids hidden behavior.
---
What
safe-timeouts does NOT doIt is important to be explicit about limitations:
* ❌ It cannot forcibly stop JavaScript execution
* ❌ It cannot cancel non-abort-aware libraries
* ❌ It cannot stop CPU-bound loops
* ❌ It does not replace DB-level timeouts
This matches the realities of Node.js and modern async runtimes.
---
How this differs from
setTimeout| Feature | setTimeout | safe-timeouts |
| ------------------- | ---------- | ------------ |
| End-to-end deadline | ❌ | ✅ |
| Nested composition | ❌ | ✅ |
| AbortSignal support | ❌ | ✅ |
| Context propagation | ❌ | ✅ |
| Concurrency-safe | ❌ | ✅ |
setTimeout works locally. safe-timeouts works across your entire async call graph.---
API
$3
Runs an async function with a deadline.
`ts
withTimeout(ms: number, fn: (signal: AbortSignal) => Promise): Promise
`Rejects with
TimeoutError when the deadline is exceeded.---
$3
Error thrown when the deadline is exceeded.
`ts
instanceof TimeoutError === true
`---
When to use this
Use
safe-timeouts` when:* you want request-level deadlines
* you call multiple async services
* you rely on Axios, fetch, or streams
* you want correct nested timeout behavior
Do not use it as a replacement for DB-level query timeouts.
---
MIT