Many concurrency bugs stemmed from abort/cancellation signals not being threaded through every async call, or from async operations that could hang forever once a caller stopped waiting. When writing any multi-step async flow (provider calls, caches, delays, connection setup, streaming reads), treat the AbortSignal/cancellation token as a first-class parameter that must be:
abortSignal (or similar), pass it down to every nested async call it makes (SDK send(), cache.get(), setTimeout-based delays, websocket handshakes, etc.). Don’t silently drop it just because the current signature doesn’t accept a third argument — extend the signature.await resolves (e.g., a cache read finishing after abort). Check signal.aborted immediately after each await, and prefer Promise.race-ing the operation against the signal rather than checking it only at the start.Example of racing a setup phase against the overall deadline instead of arming it late:
async start(): Promise<ConversationResult> {
const deadline = Date.now() + this.config.timeoutMs;
const remaining = () => Math.max(0, deadline - Date.now());
await raceWithTimeout(this.setup(), remaining()); // connect + configureSession
return raceWithTimeout(this.runConversation(), remaining());
}
Applying this consistently prevents orphaned network requests after cancellation, eval runs that outlive their deadlines, and callers hanging indefinitely on a promise nobody will ever resolve.