Name-Keyed Async Task Scheduler with Sequential Lanes, Cancel and Clear by Name
Company: Airwallex
Role: Frontend Engineer
Category: Software Engineering Fundamentals
Difficulty: medium
Interview Round: Technical Screen
Create a task scheduler for asynchronous functions, built in the style of an event emitter. Each task is submitted under a name together with an async function. The scheduler must satisfy two rules:
- Tasks with the same name execute one after another: a task starts only after the previous task with that name has finished, and tasks with the same name run in the order they were scheduled.
- Tasks with different names execute concurrently and independently: a slow or failing task under one name never delays or affects tasks under another name.
Implement it in JavaScript or TypeScript. One possible starting interface, which you may extend if you explain why:
```ts
interface TaskScheduler {
schedule(name: string, task: () => Promise<unknown>): void;
on(event: string, listener: (payload: unknown) => void): void;
}
```
The interview continued with two follow-ups, given below as Parts 2 and 3.
### Clarifying Questions
- Should `schedule` also return a promise for the task's result, or are results reported only through events? Which lifecycle events should the emitter publish?
- If a task rejects or throws, should the next task with the same name still run?
- Is there any limit on how many names may run at the same time?
- Can a task call back into the scheduler, for example to schedule another task under its own name?
### Part 1 — Same-name order, cross-name concurrency
Implement `schedule` and the event interface so that both rules hold. Show exactly how the next task for a name gets started, including when the current task fails.
```hint State per name
Think about what you need to remember for each name to know whether something is already running, and what event should trigger the next task for that name.
```
```hint Failures must not stall the line
Decide what happens to the waiting tasks of a name when the running one rejects, and make sure that code path still starts the next task.
```
#### What This Part Should Cover
- Strict per-name ordering with no overlap, and full independence across names
- Starting the next task after both success and failure, including a synchronous throw
- Cleaning up per-name state once a name has nothing left to run
- How results and errors reach the caller: a returned promise, emitted events, or both
### Part 2 — Cancel the running task by name
Add `cancel(name)`: cancel the task currently running under `name` and move on to the next scheduled task with that name.
```hint You cannot stop a promise
A running async function cannot be stopped from outside. Think about what the scheduler can do on its own side, and what it could offer the task so the task can stop itself.
```
```hint The late finisher
The cancelled task's promise may still settle after the next task has started. Make sure that late settlement cannot advance the queue a second time or report a result.
```
#### Clarifying Questions for this Part
- Must cancelling stop the task's actual work, or only stop the scheduler from waiting for it?
- What should the code that scheduled the cancelled task observe?
#### What This Part Should Cover
- The precise meaning of cancellation, and how a cooperative task learns about it
- Protection against the cancelled task's late settlement
- What the cancelled task's caller and the event listeners observe
- Behavior when nothing is running under that name
### Part 3 — Clear all tasks by name
Add `clear(name)`: clear all running and scheduled tasks with that name.
```hint Clear versus cancel
Compare which parts of the per-name state each operation touches, and decide whether a task scheduled right after a clear should run.
```
```hint Nobody left waiting
Every discarded task had a caller waiting on it. Decide how each of them learns that its task will never run.
```
#### What This Part Should Cover
- Discarding the waiting tasks and settling or reporting each of them
- Cancelling the running task by reusing the Part 2 logic, in the right order
- Tasks scheduled during or after the clear
- Releasing the name's state afterwards
### What a Strong Answer Covers
- Correctness under re-entrancy: listeners or tasks that call `schedule`, `cancel` or `clear` while the scheduler is updating its state
- No unbounded growth of per-name state, and queue operations that stay cheap when many tasks are waiting
- Deterministic tests for ordering, concurrency, failure and cancellation
- A clean separation between the public API and internal state
### Follow-up Questions
- How would you add a global limit on the number of tasks running at once across all names, while keeping per-name order?
- How would you add a timeout per task, and how does it interact with `cancel`?
- How would you write deterministic unit tests for these rules without relying on real timers?
- What changes if failed tasks must be retried with backoff before the next task with the same name starts?
Overview: Implement a JavaScript task scheduler for async functions where tasks sharing a name run strictly in order while tasks with different names run concurrently. Follow-ups add cancelling the running task for a name and clearing every running and queued task by name, testing promise handling, race conditions and API design.
Read the full Airwallex Frontend Engineer interview experience this question came from