- Lab
-
Libraries: If you want this lab, consider one of these libraries.
- Core Tech
Complex Agentic Orchestration with Promise and Race Signals
In this code lab, you'll build a complex AI orchestration system using advanced Promise patterns. You'll implement a timeout controller, use Promise.race for competitive flows, collect results, and handle cancellation signals in agent workflows. When finished, you'll have production-ready patterns for orchestrating sophisticated async AI operations.
Lab Info
Table of Contents
-
Challenge
Introduction
Welcome to the Complex Agentic Orchestration with Promise and Race Signals code lab. In this hands-on lab, you will build the orchestration layer that coordinates a fleet of AI agents under hard timing constraints.
You will bound each agent with an
AbortControllerso a stalled call cannot run forever, race every agent so the fastest response wins, settle a parallel fan-out so one failed agent never discards the rest, and thread a single cancellation signal through the whole run so the orchestration stops cleanly the moment its deadline passes.About the tools and concepts
-
An
AbortControlleris the standard way to put a hard time bound on an async operation. You create a controller, hand itssignalto whatever you await, and callcontroller.abort()when the operation has run too long. Code that listens for that signal can stop when the signal fires, so a single slow agent cannot stall the run indefinitely. Pairing the controller with asetTimeoutthat firesabort()turns a fixed delay into a clean, enforceable timeout. -
AbortSignal.anycombines several signals into one. When an agent must stop on either its own local timeout or a wider cancellation from the orchestrator, you fold both signals into a single derived signal that fires as soon as the first of them does. This is how one global deadline can reach down and cancel work that also carries its own per-operation bound. -
Promise.racesettles with the first promise to finish, whether it resolves or rejects. It is the primitive for competitive flows: launch every agent at once, race them, and take whichever answers first. Adding a deadline promise as one more contender means the race rejects on its own if no agent beats the clock, so a slow fleet fails fast instead of hanging. -
Promise.allSettledis the resilient counterpart. UnlikePromise.all, which rejects the instant any one promise rejects,allSettledwaits for every promise to finish and reports each outcome as eitherfulfilledwith a value orrejectedwith a reason. One failed agent never cancels the others, which is exactly the behavior a partial-results gather needs. -
A typed error lets the orchestration reason about why an agent stopped. By surfacing a dedicated
TimeoutErrorwhen an operation hits its bound or is cancelled, the rest of the system can treat that outcome as transient rather than confusing it with a genuinely broken response.
Prerequisites
Before starting this lab, you should have:
- Understanding of Promises and async/await syntax: composing, awaiting, and resolving promises
- Familiarity with asynchronous JavaScript patterns: fan-out, fan-in, and settlement
- Knowledge of event-driven programming: registering and reacting to events such as
abort - Basic understanding of AI agents and orchestration concepts: coordinating several model or API calls toward one result
- Experience with
setTimeoutand timing in JavaScript: scheduling and clearing timers
The lab environment is ready to use. Run
node --versionfrom inside theworkspacefolder at any time to confirm the runtime.The project dependencies are already installed so Jest is available. The stack is Node.js 24.x, JavaScript ES2025, and Jest 29.x, with a mock agent standing in for the real model service so you can exercise the timing and cancellation behavior without a network or an API key.
The orchestration itself uses only Node built-ins,
AbortController,AbortSignal.any,Promise.race, andPromise.allSettled, so there is nothing extra to import. You validate each task by running its Jest test withnpx jest.
Background
The Scenario
You are a backend engineer at CarvedRock building an AI orchestration platform that coordinates several language models and external APIs at once. Each agent answers at its own pace, and some never answer in time.
The platform needs to bound every agent so a stalled call cannot block the run, select the fastest response when agents compete, collect partial results when some agents fail, and cancel everything in flight the moment the run passes its deadline.
Your task is to implement timeout control with
AbortController, orchestrate a competitive flow withPromise.race, gather mixed success and failure withPromise.allSettled, and thread a single cancellation signal through the whole orchestration.The Application Structure
Key files in the lab environment
workspace/src/config.js: the shared timing budget, the per-agent bound, the race deadline, the global deadline, and the minimum usable result count, all read from one placeworkspace/src/mockAgent.js: the mock agent that resolves after its delay or rejects when its signal fires; treat it as a black boxworkspace/src/errors.js: the typedTimeoutErrorraised when an operation is bounded out or cancelledworkspace/src/withTimeout.js: the helper that runs any operation under a hard time bound and folds in an external cancel signalworkspace/src/agentClient.js: the per-agent wrapper that runs one agent under the bound and tags its resultworkspace/src/raceAgents.js: the competitive flow that races every agent and a deadline against each otherworkspace/src/orchestrator.js: the resilient gather and the deadline-bounded orchestration that ties it all togetherworkspace/src/logger.js: the shared stage loggerworkspace/runOrchestration.js: the end-to-end runner that races the agents, then gathers them, and prints the reportworkspace/data/agents.js: the five mock agents with their delays and replies
Complete the tasks in order. Each task builds on the previous one.
Note: The orchestration will not run successfully until you complete all steps in this lab.
After you complete all tasks, you can run the full orchestration from the workspace directory with:
node runOrchestration.js -
-
Challenge
Establishing the agent client and the timeout bound
Setting the system's timing budget in one place
Every bounded orchestration starts from a handful of numbers: how long a single agent may run, how long the competitive race waits before giving up, how long the whole run may take, and how many results make a gather worth using.
Centralizing these in
config.jsmeans the timeout helper, the race, and the gather all read from one source of truth, and tuning the system later is a one-line change rather than a hunt across modules.The values you set here matter: a per-agent bound that sits above the fast agents but below the stalled ones lets the healthy responses through while cutting off the ones that never arrive, and a global deadline tighter than the slowest agent is what forces the run to settle on partial results instead of waiting forever. ### Bounding an operation and naming the timeout
The timeout helper is the single place where your code turns an open-ended async call into a bounded one, so it is also where a stalled agent gets stopped.
A private
AbortControllerarms a timer that firesabort()once the bound passes, and the operation receives the controller's signal so it can react the instant the bound is hit.Folding an external cancel signal in with
AbortSignal.anymeans the same operation also stops when a wider deadline fires, so one helper serves both the local timeout and the global cancellation.Clearing the timer once the operation settles keeps a finished call from leaving a stray timer behind.
-
Challenge
Racing agents for the fastest response
Tagging one agent's result under the bound
Before agents can compete, each one needs to run under the shared time bound and carry enough context for the report to know which agent answered.
The per-agent wrapper threads the cancel signal down into the timeout helper, so a global deadline can reach this agent mid-flight, and shapes the agent's reply into a small result object tagged with the agent's id.
That tag is what lets a race winner or a gathered result be attributed to a specific agent rather than arriving as an anonymous value.
Racing every agent against a deadline
A competitive flow launches every agent at once and takes whichever answers first.
Promise.raceis the exact primitive for this: it settles with the first promise to finish, so the fastest healthy agent wins without you tracking any of the others.Adding a deadline promise as one more contender gives the race a hard stop of its own.
If every agent is slower than the deadline, the deadline wins the race and rejects, so a sluggish fleet fails fast with a clear timeout instead of hanging while the slowest agent grinds on.
-
Challenge
Gathering partial results from parallel agents
Settling every agent instead of bailing on the first
Racing finds the single fastest answer, but most orchestration wants all the answers it can get. The choice of how you await a parallel fan-out is what makes it resilient.
Promise.allwould abandon the entire gather the instant any one agent rejected, discarding every healthy response alongside the one failure.Promise.allSettledinstead waits for every agent and reports each outcome independently, so a stalled or cancelled agent never cancels the ones that succeeded.That is the behavior a partial-results gather needs when you fully expect a few agents to miss.
Splitting the outcomes into fulfilled and rejected
A settled array is a list of outcome objects, not results: each entry reports a
statusof eitherfulfilledorrejected, with the real value or the error tucked inside.The gather's job is to reshape that into the two buckets a caller actually wants: the agent replies that came back, and the reasons the others did not.
Partitioning the settled array by status gives the orchestration an honest picture of the run, how many agents made it through and exactly which ones failed, which is the difference between a result you can trust and one you can only guess at.
-
Challenge
Cancellation signals across the orchestration
Cancelling the whole run with one shared signal and observing the cancellation
A gather that settles every agent is resilient, but on its own it still waits for the slowest agent to finish or time out.
A global deadline is what caps the total run.
One
AbortControllerbecomes the single cancellation signal for the entire orchestration: a timer fires itsabort()once the budget is spent, and because that same signal was threaded down into every agent through the gather, every in-flight agent stops together. Clearing the deadline timer in afinallyblock keeps a run that finishes early from leaving a timer pending.A production orchestration needs to do more than fire the deadline. It needs to know whether the deadline actually fired, because a run that was cut short by its budget is a different event from one that finished on its own, even when both return results.
An
AbortControllerexposes that distinction through its signal: the signal emits anabortevent the momentabort()is called, so registering a one-time listener that flips a flag turns the cancellation into something the report can observe rather than something that happens silently.Capturing a timestamp at the start of the run gives you the other half of the picture, the elapsed time, which you turn into a reported duration in the next task.
Listening on the signal rather than inspecting the timer is what lets a single observation cover both the deadline firing and any other source that might abort the same controller.
Judging the outcome and assembling a richer report
The final task turns the gather's two buckets and the cancellation you tracked into the answer a caller can act on.
A usable verdict compares how many agents returned against the minimum you set, so the caller learns at a glance whether enough of the fleet answered to trust the result.
A status turns the
cancelledflag into a plain word,cancelledwhen the deadline cut the run short orcompletewhen it finished on its own, and the two are independent: a run can be cancelled by its deadline and still be usable because enough agents answered before the budget ran out.A duration derived from the start time tells the caller how long the gather actually took.
Assembling the verdict, the status, the duration, the fulfilled values, and the failure reasons into one object produces the honest, observable report a real platform would hand to whatever consumes the orchestration, not just what came back but how the run got there.
-
Challenge
Run the full orchestration
Now that every task is complete, run the end-to-end orchestration to watch the race pick a winner and the gather settle every agent under the global deadline.
-
Confirm the runtime is available:
node --version -
Start the orchestration against the full set of agents from
workspace:node runOrchestration.js -
Watch the log stream print an
[INIT]line as the dispatch begins, then a[RACE]line naming the fastest agent, then three[GATHER]lines reporting the usable verdict, the run status and gather duration, and the fulfilled-and-failed counts, and finally a[DONE]line reporting how long the whole run took. -
Notice the
[RACE]line: the fastest agent wins the competitive flow and is named with its reply, well before the slower agents would have answered. -
Confirm the
[GATHER]summary reports usable: true, status: cancelled, a gather duration of roughly 1800 milliseconds, and fulfilled: 3 with failed: 2.
The three agents that beat the global deadline return their replies, and the two that run past it are cancelled together by the shared signal, which is exactly the partial-results outcome the gather is built to handle.
Notice thatusableandstatusdisagree on purpose: the deadline cut the run short, so its status iscancelled, yet enough agents answered in time for the result to still be usable. Because the mock agents respond on fixed delays, the run is deterministic and settles in roughly 1.8 seconds, paced by the global deadline rather than the slowest agent.
Expected result: Every layer you built is visible in one run: the timeout helper bounds each agent, the race picks the fastest responder,
Promise.allSettledgathers a mix of success and failure without bailing out, and one shared cancellation signal cuts off the agents that overrun the deadline, settling the whole orchestration into a clean report with a usable verdict, three fulfilled results, and two failed results instead of waiting on the slowest agent. -
-
Challenge
Conclusion
Congratulations on completing the Complex Agentic Orchestration with Promise and Race Signals lab!
You have built the orchestration layer that turns a flat set of async agent calls into a bounded, competitive, and resilient system: timing out a stalled agent with
AbortController, racing the fleet for the fastest answer, settling a parallel gather into partial results, and threading one cancellation signal through the whole run.These are the patterns you need to safely orchestrate sophisticated async AI operations.
What you have accomplished
- Set the Timeout, Race, and Deadline Limits: Defined the per-agent bound, the race deadline, the global deadline, and the usable threshold once in a shared config every module reads from.
- Bounded an Operation with AbortController: Built a helper that runs any operation under a hard time bound, folds in an external cancel signal, and clears its timer when the operation settles.
- Ran One Agent and Tagged Its Result: Routed a single agent through the bound and shaped its reply into a result tagged with the agent's id.
- Raced the Agents and the Deadline: Used
Promise.raceto take the fastest responder and a deadline contender to fail fast when the whole fleet is slow. - Launched Every Agent and Settled the Batch: Used
Promise.allSettledso one failed agent never cancels the gather. - Partitioned the Settled Outcomes: Split the settled array into fulfilled replies and rejected reasons for an honest breakdown.
- Gathered Under a Global Deadline and Tracked the Cancellation: Threaded one
AbortControllersignal through the gather so a single deadline cancels every in-flight agent together, and registered anabortlistener that records whether the deadline fired. - Judged the Run and Assembled the Full Report: Compared the result count against the minimum, derived a status that tells a cancellation apart from a clean finish, measured the elapsed duration, and returned a complete observable report.
Key takeaways
- An
AbortControllerpaired with asetTimeoutis the standard way to put a hard, enforceable bound on an async operation, so a single stalled agent can never block the run. AbortSignal.anylets one operation stop on either its own timeout or a wider cancellation, which is how a global deadline reaches down into work that also carries its own per-operation bound.Promise.raceis the primitive for competitive flows: it takes the first to settle, and a deadline contender turns no answer in time into a clean rejection rather than a hang.Promise.allSettledis the resilient way to await a fan-out: it reports every outcome instead of abandoning the gather on the first rejection.- One shared cancellation signal threaded through every agent is what lets a single deadline cancel the entire orchestration at once, so the run settles on partial results instead of waiting on its slowest member.
- An
AbortControllersignal emits anabortevent, so a one-time listener can turn a silent cancellation into something the report observes, letting a run distinguish a deadline-cut result from a clean finish even when both return usable data.
Experiment before you go
You still have time in the lab environment. Try these explorations:
- Raise
GLOBAL_DEADLINE_MSabove the slowest agent's delay and rerun the orchestration: watch the failed bucket shrink as more agents beat the deadline, the reported status flip fromcancelledtocompletebecause the deadline no longer fires, and the gather duration climb to match. - Lower
RACE_DEADLINE_MSbelow the fastest agent's delay and watch the[RACE]line report that no agent beat the deadline, with the deadline contender winning the race instead. - Raise
MIN_RESULTSabove the number of agents that finish in time and watch theusableverdict flip tofalseeven though real replies came back. - Lower
AGENT_TIMEOUT_MSbelow a mid-speed agent's delay and watch that agent move from the fulfilled bucket to the failed one as its own bound cuts it off before the global deadline does. - Add a delay to a fast agent in
data/agents.jsand watch the race winner change, since the fastest responder is whatever answers first.
About the author
Real skill practice before real-world application
Hands-on Labs are real environments created by industry experts to help you learn. These environments help you gain knowledge and experience, practice without compromising your system, test without risk, destroy without fear, and let you learn from your mistakes. Hands-on Labs: practice your skills before delivering in the real world.
Learn by doing
Engage hands-on with the tools and technologies you’re learning. You pick the skill, we provide the credentials and environment.
Follow your guide
All labs have detailed instructions and objectives, guiding you through the learning process and ensuring you understand every step.
Turn time into mastery
On average, you retain 75% more of your learning if you take time to practice. Hands-on labs set you up for success to make those skills stick.