Express.js / Intermediate: modules, identity and adapters / Lesson 8
Async services, dependencies and source budgets
Keep source work connected to the request while bounding concurrency, deadlines and retry ownership.
On this page
Lesson goals
- Distinguish awaited service work from detached background tasks.
- Preserve a shared deadline instead of resetting a full timeout at each hop.
Let the route adapt HTTP and the service own the use case
A route should translate validated request input and principal context into a domain call, then map the result to HTTP. The service owns coherent business behavior and its source requirements. This separation makes the same use case available to another adapter without copying authorization into every endpoint. Async does not make CPU-heavy JavaScript nonblocking, nor does it impose a maximum number of remote calls. Review actual source fanout, pool queueing and work size. Keep configuration and long-lived source clients outside the per-request constructor while keeping principal-dependent caches inside their correct request lifetime.
Pass an explicit operation context
The fragment passes a principal and an injected request deadline to a service. Express does not create these custom properties or automatically enforce the deadline. The service and source adapter must honor it using supported cancellation and timeout mechanisms. Awaiting the call keeps rejection in the Express 5 promise path. Starting the call without awaiting or returning it would require another explicit owner for completion and failure. A durable background job needs a real queue protocol and result identity, not a detached promise whose process may exit before completion.
app.get("/courses/:id/card", async (req,res) => {
const card = await catalog.card({
id:req.params.id, principal:req.principal,
deadlineMs:req.operationDeadlineMs
});
return res.json(card);
});
principal and operationDeadlineMs are injected application contracts. The snippet does not configure source timeouts or implement cancellation.
Budget remaining time with a finalization reserve
A shared deadline prevents a chain of calls from each receiving a fresh full timeout. The complete arithmetic model reserves part of the remaining time for serialization and cleanup. Its numbers are synthetic and make no recommendation about acceptable production latency. If no source budget remains, the adapter should follow the declared deadline failure rather than silently using an unlimited timeout. A real driver may not support cancellation, so record that limitation and observe whether timed-out work actually stops. Client timeout and source interruption are separate facts.
function sourceBudget(deadline,now,reserve) {
return Math.max(0,deadline-now-reserve);
}
console.assert(sourceBudget(1000,900,20) === 80);
console.assert(sourceBudget(1000,990,20) === 0);
console.assert(sourceBudget(1000,1100,20) === 0);
The three budgets are 80, 0 and 0 in the same synthetic time unit. The function does not stop an external operation.
Bound concurrent work without confusing Promise.all with control
Promise.all can wait for several operations, but starting a promise for every item first can overwhelm a pool before any await occurs. Bound input cardinality and use a reviewed concurrency strategy at the source adapter. When one promise rejects, other already-started work is not automatically canceled. Decide whether independent results may become partial data or whether the use case requires all sources. That decision affects status, error disclosure and cleanup. Avoid hiding a failed permission source behind a permissive default simply because a descriptive fallback would be acceptable elsewhere.
Give retries one owner and preserve uncertain writes
Client, route and source retries can multiply attempts if each layer independently applies its own policy. Choose a retry owner, retryable categories, bounded attempts and a remaining-deadline check. A read that fails before response may be safely repeatable under its domain semantics, while a write timeout can occur after commit. Do not advertise exactly-once effects from an HTTP handler or assume a thrown exception proves no storage change. Use a domain request identity and reconciliation protocol for writes; keep those guarantees out of a generic retry wrapper that cannot know the operationโs effects.
Common errors and fixes
A route reports success while its detached work fails
- Why it happens
- The source promise had no awaited or durable owner.
- How to fix it
- Await request work or enqueue through an explicit durable protocol.
- Prevent it next time
- Test source rejection before returning a success result.
Every hop restarts a full timeout
- Why it happens
- There was no propagated operation deadline.
- How to fix it
- Pass and honor the remaining shared budget.
- Prevent it next time
- Test exhausted budget before starting source work.
Promise rejection leaves other calls running
- Why it happens
- Promise aggregation was mistaken for cancellation.
- How to fix it
- Use supported cancellation and explicit cleanup ownership.
- Prevent it next time
- Observe resource release in isolated adapter integration.
Check your understanding
1. What does the budget function fail to prove?
Show worked solution
It proves only arithmetic for declared time values. It does not establish a clock source, enforce a timeout, interrupt a database query or release a pool slot. Those claims require the actual adapter and driver integration. Keep the function test as narrow evidence and separately verify deadline propagation and cleanup where authorized.
2. Why can one Promise.all rejection still leave expensive work active?
Show worked solution
The other operations may already have started, and rejecting the aggregate does not cancel them. Their owners must propagate supported cancellation and release resources. Also cap work before starting it; a concurrency policy applied only after creating every promise cannot undo the initial fanout.
Key takeaways
- An awaited promise needs a source budget and resource owner.
- Retry and partial-result policy follow domain semantics.