Module 10: Promises (JS) & Asyncio (Python)

This is the one module where the two languages genuinely diverge in mechanics, not just syntax — but the underlying ideas (a value that arrives later, waiting for one or many of them, racing, timing out, limiting how many run at once) are identical. JavaScript's Promise/async/await is matched here against Python's asyncio, function for function.

Module 10 of 12 Problems 91–100 JS + Python ~60 Min

By the end of this module, you'll be able to

  • Explain why Promise.all/asyncio.gather is faster than awaiting tasks one at a time
  • Match every major Promise static method to its closest asyncio equivalent
  • Add a timeout and a concurrency limit to a batch of async operations in either language

1. Problems 91–100

Same format as the previous modules: expand a problem to see the approach and both commented solutions. Several problems here reuse a shared helper — a task(id, duration) function that waits, then returns a label — to keep the timing comparisons easy to follow.

P91

Sleep / Delay

Wait 2 seconds, then continue.

Approach: JavaScript has no built-in sleep — wrap setTimeout in a Promise that resolves once the timer fires, then await it. Python's asyncio ships sleep() directly.

JavaScript
sleep.js
// A Promise that resolves after a timeout is the standard way to "await" time
// passing in JavaScript -- there's no built-in sleep() function.
function sleep(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

async function demo() {
  console.log("Waiting...");
  await sleep(2000);
  console.log("Executed after 2 seconds");
}

demo();
Python
sleep.py
import asyncio

async def demo():
    print("Waiting...")
    await asyncio.sleep(2)  # Python's asyncio ships sleep() directly -- no wrapper needed
    print("Executed after 2 seconds")

asyncio.run(demo())
P92

Create and Resolve a Promise Manually

Reject on an invalid ID, resolve with a name otherwise.

Approach: a new Promise(executor) runs its executor immediately; calling resolve() or reject() settles it. A Python coroutine is the closest equivalent — defining it doesn't run it, awaiting it does, and raising an exception is how it "rejects."

JavaScript
fetch-user-name.js
// The executor function runs immediately; calling resolve() or reject() settles
// the Promise, which .then()/await can react to afterward.
function fetchUserName(userId) {
  return new Promise((resolve, reject) => {
    if (userId <= 0) {
      reject(new Error("Invalid user ID"));
    } else {
      resolve(`User${userId}`);
    }
  });
}

async function demo() {
  const name = await fetchUserName(42);
  console.log(name); // "User42"
}

demo();
Python
fetch_user_name.py
import asyncio

async def fetch_user_name(user_id: int) -> str:
    """A coroutine is Python's Promise equivalent -- calling it doesn't run it yet;
    awaiting it does, and raising an exception is how it "rejects"."""
    if user_id <= 0:
        raise ValueError("Invalid user ID")
    return f"User{user_id}"

async def demo():
    name = await fetch_user_name(42)
    print(name)  # "User42"

asyncio.run(demo())
P93

Run Async Tasks Sequentially

Three 300ms tasks, one after another, take ~900ms total.

Approach: awaiting each call before starting the next means the second task doesn't even begin until the first has fully finished — their durations simply add up.

JavaScript
run-sequentially.js
// Awaiting each call before starting the next means task 2 doesn't begin until
// task 1 has fully finished -- their delays add up.
async function task(id, ms) {
  await sleep(ms);
  return `Task ${id} done`;
}

async function runSequentially() {
  const results = [];
  results.push(await task(1, 300));
  results.push(await task(2, 300));
  results.push(await task(3, 300));
  return results; // takes roughly 900ms total
}
Python
run_sequentially.py
import asyncio

async def task(task_id: int, seconds: float) -> str:
    await asyncio.sleep(seconds)
    return f"Task {task_id} done"

async def run_sequentially() -> list:
    results = []
    results.append(await task(1, 0.3))
    results.append(await task(2, 0.3))
    results.append(await task(3, 0.3))
    return results  # takes roughly 0.9s total
P94

Run Async Tasks in Parallel

The same three tasks, started together, take ~300ms total.

Approach: Promise.all/asyncio.gather start every task immediately instead of one at a time, then wait for all of them together — the total time becomes roughly the slowest single task, not the sum of all of them.

JavaScript
run-in-parallel.js
// Promise.all() starts every task immediately and waits for all of them --
// the total time is roughly the SLOWEST task, not the sum of all of them.
async function runInParallel() {
  const results = await Promise.all([
    task(1, 300),
    task(2, 300),
    task(3, 300),
  ]);
  return results; // takes roughly 300ms total, not 900ms
}
Python
run_in_parallel.py
import asyncio

async def run_in_parallel() -> list:
    """asyncio.gather() is Python's Promise.all() -- run everything concurrently,
    wait for all of it, and get results back in the original order."""
    results = await asyncio.gather(
        task(1, 0.3),
        task(2, 0.3),
        task(3, 0.3),
    )
    return results  # takes roughly 0.3s total, not 0.9s
P95

Race Multiple Tasks, Take Whichever Finishes First

A 500ms task and a 100ms task race — the fast one wins.

Approach: Promise.race settles the instant the first promise settles, win or lose — the rest keep running in the background but their results are ignored. asyncio needs one extra step: cancelling the losing tasks yourself.

JavaScript
race-example.js
// Promise.race() settles as soon as the FIRST promise settles -- the others
// keep running in the background but their results are ignored.
async function raceExample() {
  const winner = await Promise.race([
    task("slow", 500),
    task("fast", 100),
  ]);
  return winner; // "Task fast done"
}
Python
race_example.py
import asyncio

async def race_example() -> str:
    """asyncio.wait(..., return_when=FIRST_COMPLETED) is the closest match to
    Promise.race() -- it also leaves you to cancel the losers yourself."""
    tasks = [asyncio.create_task(task("slow", 0.5)), asyncio.create_task(task("fast", 0.1))]
    done, pending = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
    for p in pending:
        p.cancel()  # Promise.race() doesn't cancel losers automatically -- asyncio does need this
    return done.pop().result()  # "Task fast done"
P96

Handle Multiple Failures Without Stopping Early

One task fails, two succeed — report all three outcomes.

Approach: Promise.all/plain gather() abort the moment any one task fails. Promise.allSettled waits for every task regardless and reports each one's individual outcome — Python's version is gather(return_exceptions=True), which returns failures as exception objects instead of raising.

JavaScript
settle-all.js
// Promise.all() rejects immediately if ANY promise rejects. Promise.allSettled()
// instead waits for every promise and reports each one's outcome individually.
async function settleAll() {
  const results = await Promise.allSettled([
    Promise.resolve("ok"),
    Promise.reject(new Error("failed")),
    Promise.resolve("ok too"),
  ]);
  return results.map((r) => r.status); // ["fulfilled", "rejected", "fulfilled"]
}
Python
settle_all.py
import asyncio

async def _ok():
    return "ok"

async def _fails():
    raise ValueError("failed")

async def settle_all() -> list:
    """return_exceptions=True turns gather() into allSettled(): failures come back
    as exception objects in the results list instead of raising immediately."""
    results = await asyncio.gather(_ok(), _fails(), _ok(), return_exceptions=True)
    return ["rejected" if isinstance(r, Exception) else "fulfilled" for r in results]
    # ["fulfilled", "rejected", "fulfilled"]
P97

Add a Timeout to an Async Operation

Give up on a 2-second task if it hasn't finished in 500ms.

Approach: JavaScript builds a timeout by racing the real operation against a Promise that rejects after a deadline — whichever settles first wins. Python's asyncio.wait_for() bakes that exact pattern in as a single call.

JavaScript
with-timeout.js
// Race the real operation against a Promise that rejects after a deadline --
// whichever settles first wins.
function withTimeout(promise, ms) {
  const timeout = new Promise((_, reject) =>
    setTimeout(() => reject(new Error("Timed out")), ms)
  );
  return Promise.race([promise, timeout]);
}

async function demo() {
  try {
    const result = await withTimeout(task("slow", 2000), 500);
    console.log(result);
  } catch (err) {
    console.log(err.message); // "Timed out"
  }
}
Python
with_timeout.py
import asyncio

async def demo():
    try:
        # asyncio.wait_for() bakes the race-against-a-deadline pattern in directly.
        result = await asyncio.wait_for(task("slow", 2), timeout=0.5)
        print(result)
    except asyncio.TimeoutError:
        print("Timed out")

asyncio.run(demo())
P98

Retry a Failing Async Operation

Module 9's retry(), rewritten for an async task.

Approach: identical structure to Module 9's synchronous retry(), but each attempt is awaited, since the operation being retried is itself asynchronous.

JavaScript
retry-async.js
// Same idea as the synchronous retry() from Module 9, but every attempt is
// awaited since the operation itself is asynchronous.
async function retryAsync(fn, maxAttempts) {
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      return await fn();
    } catch (err) {
      if (attempt === maxAttempts) throw err;
      console.log(`Attempt ${attempt} failed, retrying...`);
    }
  }
}
Python
retry_async.py
async def retry_async(fn, max_attempts: int):
    for attempt in range(1, max_attempts + 1):
        try:
            return await fn()
        except Exception:
            if attempt == max_attempts:
                raise
            print(f"Attempt {attempt} failed, retrying...")
P99

Limit Concurrency (Promise Pool)

Run 100 tasks, but never more than 5 at the same time.

Approach: instead of starting every task at once, cap how many run concurrently — each finished task frees a slot for the next one to start. Python's asyncio.Semaphore implements exactly this limiter as a reusable primitive.

JavaScript
promise-pool.js
// Instead of starting every task at once, cap how many run at the same time --
// each finished task pulls the next one off the queue.
async function promisePool(tasks, limit) {
  const results = [];
  const executing = new Set();
  for (const [index, taskFn] of tasks.entries()) {
    const p = taskFn().then((result) => { results[index] = result; executing.delete(p); });
    executing.add(p);
    if (executing.size >= limit) {
      await Promise.race(executing); // wait for a slot to free up before adding more
    }
  }
  await Promise.all(executing);
  return results;
}
Python
promise_pool.py
import asyncio

async def promise_pool(task_fns: list, limit: int) -> list:
    """A Semaphore is Python's built-in concurrency limiter: acquire() blocks once
    `limit` tasks are already running, and release() lets the next one through."""
    semaphore = asyncio.Semaphore(limit)

    async def run_with_limit(task_fn):
        async with semaphore:
            return await task_fn()

    return await asyncio.gather(*(run_with_limit(fn) for fn in task_fns))
P100

Async Queue — Process Items One at a Time

A worker loop that handles one queued item after another.

Approach: a simple worker loop — pull the next item off the front of the queue and await its handler before moving on to the one after it. This is concurrency limit 1 as a special case of problem 99.

JavaScript
process-queue.js
// A simple worker loop: pull items off the front of the queue and await each
// one's processing before moving to the next.
async function processQueue(items, handler) {
  const results = [];
  const queue = [...items];
  while (queue.length > 0) {
    const item = queue.shift(); // take the next item off the front
    results.push(await handler(item));
  }
  return results;
}
Python
process_queue.py
import asyncio

async def process_queue(items: list, handler) -> list:
    """collections.deque would be more efficient than a list for repeated pop(0),
    but the shape of the algorithm is identical either way."""
    results = []
    queue = list(items)
    while queue:
        item = queue.pop(0)  # take the next item off the front
        results.append(await handler(item))
    return results

2. Key Takeaways

  • Sequential vs. parallel is the single biggest lever on total runtime: await-ing tasks one at a time adds their durations; Promise.all/asyncio.gather runs them concurrently and takes roughly the slowest one.
  • Every Promise static method has a direct asyncio counterpart: allgather, racewait(FIRST_COMPLETED), allSettledgather(return_exceptions=True) — the concepts transfer even though the APIs look different.
  • A concurrency limit (Semaphore/pool) exists to protect a downstream resource — a rate-limited API, a database connection pool — from being hit by every task at once, even when nothing stops you from launching them all simultaneously.