Featured resource
2026 Tech Forecast
2026 Tech Forecast

1,500+ tech insiders, business leaders, and Pluralsight Authors share their predictions on what’s shifting fastest and how to stay ahead.

Download the forecast
  • Lab
    • Libraries: If you want this lab, consider one of these libraries.
    • Core Tech
Labs

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 platform
Lab Info
Level
Advanced
Last updated
Jun 12, 2026
Duration
30m

Contact sales

By clicking submit, you agree to our Privacy Policy and Terms of Use, and consent to receive marketing emails from Pluralsight.
Table of Contents
  1. 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 AbortController so 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 AbortController is the standard way to put a hard time bound on an async operation. You create a controller, hand its signal to whatever you await, and call controller.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 a setTimeout that fires abort() turns a fixed delay into a clean, enforceable timeout.

    • AbortSignal.any combines 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.race settles 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.allSettled is the resilient counterpart. Unlike Promise.all, which rejects the instant any one promise rejects, allSettled waits for every promise to finish and reports each outcome as either fulfilled with a value or rejected with 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 TimeoutError when 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 setTimeout and timing in JavaScript: scheduling and clearing timers

    The lab environment is ready to use. Run node --version from inside the workspace folder 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, and Promise.allSettled, so there is nothing extra to import. You validate each task by running its Jest test with npx 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 with Promise.race, gather mixed success and failure with Promise.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 place
    • workspace/src/mockAgent.js: the mock agent that resolves after its delay or rejects when its signal fires; treat it as a black box
    • workspace/src/errors.js: the typed TimeoutError raised when an operation is bounded out or cancelled
    • workspace/src/withTimeout.js: the helper that runs any operation under a hard time bound and folds in an external cancel signal
    • workspace/src/agentClient.js: the per-agent wrapper that runs one agent under the bound and tags its result
    • workspace/src/raceAgents.js: the competitive flow that races every agent and a deadline against each other
    • workspace/src/orchestrator.js: the resilient gather and the deadline-bounded orchestration that ties it all together
    • workspace/src/logger.js: the shared stage logger
    • workspace/runOrchestration.js: the end-to-end runner that races the agents, then gathers them, and prints the report
    • workspace/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
    
  2. 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.js means 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 AbortController arms a timer that fires abort() 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.any means 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.

  3. 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.race is 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.

  4. 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.all would abandon the entire gather the instant any one agent rejected, discarding every healthy response alongside the one failure.

    Promise.allSettled instead 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 status of either fulfilled or rejected, 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.

  5. 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 AbortController becomes the single cancellation signal for the entire orchestration: a timer fires its abort() 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 a finally block 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 AbortController exposes that distinction through its signal: the signal emits an abort event the moment abort() 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 cancelled flag into a plain word, cancelled when the deadline cut the run short or complete when 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.

  6. 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.

    1. Confirm the runtime is available:

      node --version
      
    2. Start the orchestration against the full set of agents from workspace:

      node runOrchestration.js
      
    3. 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.

    4. 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.

    5. 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 that usable and status disagree on purpose: the deadline cut the run short, so its status is cancelled, 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.allSettled gathers 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.

  7. 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

    1. 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.
    2. 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.
    3. 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.
    4. Raced the Agents and the Deadline: Used Promise.race to take the fastest responder and a deadline contender to fail fast when the whole fleet is slow.
    5. Launched Every Agent and Settled the Batch: Used Promise.allSettled so one failed agent never cancels the gather.
    6. Partitioned the Settled Outcomes: Split the settled array into fulfilled replies and rejected reasons for an honest breakdown.
    7. Gathered Under a Global Deadline and Tracked the Cancellation: Threaded one AbortController signal through the gather so a single deadline cancels every in-flight agent together, and registered an abort listener that records whether the deadline fired.
    8. 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 AbortController paired with a setTimeout is the standard way to put a hard, enforceable bound on an async operation, so a single stalled agent can never block the run.
    • AbortSignal.any lets 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.race is 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.allSettled is 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 AbortController signal emits an abort event, 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_MS above the slowest agent's delay and rerun the orchestration: watch the failed bucket shrink as more agents beat the deadline, the reported status flip from cancelled to complete because the deadline no longer fires, and the gather duration climb to match.
    • Lower RACE_DEADLINE_MS below 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_RESULTS above the number of agents that finish in time and watch the usable verdict flip to false even though real replies came back.
    • Lower AGENT_TIMEOUT_MS below 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.js and watch the race winner change, since the fastest responder is whatever answers first.
About the author

Angel Sayani is a Certified Artificial Intelligence Expert®, CEO of IntellChromatics, author of two books in cybersecurity and IT certifications, world record holder, and a well-known cybersecurity and digital forensics expert.

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.

Get started with Pluralsight