
Who stops a request that keeps waiting?
A web request can receive headers promptly and still wait a long time for its body. An endless loading indicator confuses visitors and retains resources unnecessarily. A response deadline and user cancellation express different decisions: one limits the application's waiting budget, while the other reflects a changed user action or a page being abandoned. This tutorial combines both signals in an explicit contract instead of relying on an unbounded request.
Each call creates a timeout signal and combines it with the caller's signal when one is provided. Either cancellation source can interrupt receiving the response. Choose the budget according to product needs and execution environment. Browser suspension and active-time behavior can differ from a simple wall-clock measurement. A client deadline is not a substitute for server-side timeout settings, connection-capacity controls or a decision about what happens to work already running remotely.
Combine a user signal with a deadline
A resolved fetch promise does not guarantee a successful HTTP status. The helper checks status before interpreting content and gives server failures an identifiable status property. The same cancellation signal remains relevant while JSON is read, so a slow body is not silently excluded from the deadline. User aborts, timeout events, unsuccessful status codes and invalid data deserve distinct interface handling rather than an ambiguous universal failure message.
Save the four complete files together. The executable example requires Node.js 22 or newer and installs no external package. Run node --test test_deadline.mjs, then use separate terminals for node server.mjs and node demo.mjs. The successful demonstration returns an object with ready set to true. Its server listens on the loopback interface only, providing a controlled fixture rather than a production public endpoint with authentication and traffic management.
The test deliberately sends response headers before delaying the body. This checks that waiting is bounded beyond the first response stage. It also exercises an HTTP failure and a caller signal that has already been aborted. Passing under the tested runtime does not establish compatibility with older browsers. Review the actual browser matrix and separately test any fallback implementation against the same behavior before adding it to a deployed user interface.
Complete executable files
The four files are complete. The article provides prerequisites, command order and verification; code is neither shortened nor hidden.
1. Complete fetch_deadline.mjs
Save this entire file beside the others using the exact filename.
export async function fetchJson(url, {timeoutMs = 2000, signal} = {}) {
if (!Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 60000) throw new RangeError('timeoutMs must be 1..60000');
const deadline = AbortSignal.timeout(timeoutMs);
const combined = signal ? AbortSignal.any([signal, deadline]) : deadline;
const response = await fetch(url, {signal: combined});
if (!response.ok) {
await response.body?.cancel();
const error = new Error(`HTTP ${response.status}`);
error.status = response.status;
throw error;
}
return response.json();
}The execution section explains this file’s command, expected outcome and limits.
2. Complete server.mjs
Save this entire file beside the others using the exact filename.
import {createServer} from 'node:http';
import {pathToFileURL} from 'node:url';
export function makeServer() {
return createServer((req, res) => {
const url = new URL(req.url, 'http://localhost');
const delay = Math.max(0, Math.min(500, Number(url.searchParams.get('delay')) || 0));
res.writeHead(url.pathname === '/fail' ? 500 : 200, {'Content-Type': 'application/json'});
res.flushHeaders();
setTimeout(() => { if (!res.destroyed) res.end('{"ready":true}'); }, delay);
});
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
makeServer().listen(8089, '127.0.0.1', () => console.log('http://127.0.0.1:8089'));
}The execution section explains this file’s command, expected outcome and limits.
3. Complete demo.mjs
Save this entire file beside the others using the exact filename.
import {fetchJson} from './fetch_deadline.mjs';
console.log(await fetchJson('http://127.0.0.1:8089/?delay=50', {timeoutMs: 1000}));The execution section explains this file’s command, expected outcome and limits.
4. Complete test_deadline.mjs
Save this entire file beside the others using the exact filename.
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {once} from 'node:events';
import {fetchJson} from './fetch_deadline.mjs';
import {makeServer} from './server.mjs';
test('success, HTTP failure, body timeout and caller cancellation', async () => {
const server = makeServer().listen(0, '127.0.0.1');
await once(server, 'listening');
const base = `http://127.0.0.1:${server.address().port}`;
try {
assert.deepEqual(await fetchJson(base), {ready: true});
await assert.rejects(fetchJson(base + '/fail'), error => error.status === 500);
await assert.rejects(fetchJson(base + '/?delay=200', {timeoutMs: 20}), error => ['AbortError', 'TimeoutError'].includes(error.name));
const controller = new AbortController(); controller.abort();
await assert.rejects(fetchJson(base, {signal: controller.signal}), error => error.name === 'AbortError');
} finally {
server.closeAllConnections(); await new Promise(resolve => server.close(resolve));
}
});The execution section explains this file’s command, expected outcome and limits.
Test headers, response bodies and HTTP status failures
In an application, cancel obsolete requests when navigating away or starting a newer search, and prevent late results from replacing newer content. Stopping client reception does not necessarily terminate a server computation or reverse a data change. Sensitive operations still require request identifiers and server-side guarantees. Evaluate predictable completion and a clear visitor experience, rather than assuming that choosing the smallest timeout number automatically creates a reliable or faster service.
Implementation checklist
- Keep the deadline relevant while reading the body.
- Separate HTTP status failures from cancellation.
- Test user aborts and obsolete responses.
- Do not equate client cancellation with server rollback.
References
Editorial guidance: Liyan Knowledge.




