skip to content

In RxJS, how do asyncScheduler, asapScheduler and queueScheduler differ in when they run a task scheduled with zero delay?

level: middleimportance: should knowfreq 35%

answer

  1. timer, microtask, trampoline
  2. which one runs synchronously
  3. delay above zero changes two of them
  4. time operators pick one by default

basics

~20 s

With zero delay, RxJS queueScheduler runs the task synchronously (queuing nested tasks until the current one ends), asapScheduler runs it as a microtask after the current code, and asyncScheduler runs it on a timer, after pending microtasks.

solid answer

~40 s

`queueScheduler` is a **trampoline**: a zero-delay task runs synchronously, right when it is scheduled, but a task scheduled from inside a running queue task waits until the current one finishes, so recursion does not grow the call stack. `asapScheduler` schedules on the **microtask** queue, the one promises use, so the task runs after the current synchronous code but before timers. `asyncScheduler` uses a **timer** (`setInterval` under the hood), so the task runs later still. With a delay above zero, `queueScheduler` and `asapScheduler` both fall back to `asyncScheduler`'s timer behaviour. By default RxJS uses no scheduler at all for synchronous creation functions, and `asyncScheduler` for time-based operators such as `interval`, `timer`, `delay` and `debounceTime`. Passing a scheduler to `of` or `from` is deprecated in RxJS 7; `scheduled(input, scheduler)` replaces it.

go deeper

for a junior

Recall that of() emits synchronously while interval and timer run on asyncScheduler, and that schedulers exist to control when work runs.

for a middle

Explain the zero-delay behaviour of queue, asap and async, predict the ordering of mixed tasks, and name the scheduled() replacement for the deprecated argument.

for a senior

Choose a scheduler deliberately, for instance asap to defer a synchronous emission before render, and replace real time with virtual time in tests.

for a principal

Judge when explicit scheduling is justified at all, since most application code should rely on RxJS defaults and keep timing concerns out of domain logic.

## What a scheduler is in RxJS An RxJS **scheduler** decides **when** a piece of work runs. It has three parts: a queue of tasks, an **execution context** (run now, in a microtask, on a timer, before the next repaint) and a **clock** exposed as `now()`. Operators that deal with time or concurrency accept an optional scheduler argument, usually the last one, and the static `schedule(work, delay, state)` method lets you use one directly. ## The three general-purpose schedulers | Scheduler | Zero-delay task runs | Delay above zero | Typical use | |---|---|---|---| | `queueScheduler` | synchronously, as a trampoline | like `asyncScheduler` | iteration, recursive scheduling | | `asapScheduler` | in a microtask after the current code | like `asyncScheduler` | making a synchronous source asynchronous as early as possible | | `asyncScheduler` | on a timer (`setInterval`) | on a timer | anything measured in time | ### queueScheduler, the trampoline - Scheduling a zero-delay task when no queue task is running executes it **immediately and synchronously**. - Scheduling from **inside** a running queue task pushes the new task onto a queue; it runs after the current task returns. - The result is that a recursively rescheduled task runs as a flat loop, `before 3, after 3, before 2, after 2`, instead of nesting `before 3, before 2, before 1, after 1...`. That prevents stack growth in recursive scheduling. ### asapScheduler, the microtask - A zero-delay task is pushed to the scheduler's queue and a single microtask is requested (RxJS uses a resolved `Promise` for this), which flushes every queued asap task. - It therefore runs after the currently executing synchronous code, **before** any timer callback. The RxJS docs call it the best candidate for "deferring" work that people used to do with `setTimeout(fn, 0)`. - Tasks already queued on `asapScheduler` run first, so it is not a guarantee of being next. ### asyncScheduler, the timer - Every task is backed by a timer, even with zero delay, so it runs only after the current macrotask and all pending microtasks. - It is the **default** for every time-based operator: `interval`, `timer`, `delay`, `debounceTime`, `throttleTime`, `auditTime`, `sampleTime`, `bufferTime`, `timeout`. ## Ordering in practice ```ts import { asapScheduler, asyncScheduler, queueScheduler } from 'rxjs'; asyncScheduler.schedule(() => console.log('async')); asapScheduler.schedule(() => console.log('asap')); queueScheduler.schedule(() => console.log('queue')); console.log('sync'); // queue, sync, asap, async ``` `queue` prints first because it ran during its own `schedule()` call; `asap` beats `async` even though `async` was scheduled first. ## Defaults: the principle of least concurrency RxJS picks the scheduler that introduces the least concurrency that still does the job: 1. **No scheduler** for creation functions that emit a finite set of values: `of`, `from` over an array, `range`. They emit synchronously during `subscribe()`. 2. **`asyncScheduler`** for anything involving time. 3. You opt into a different scheduler explicitly, most often only in tests or for animation. ## The RxJS 7 deprecation The `scheduler` argument of `of`, `from`, `merge`, `concat`, `combineLatest`, `startWith` and `endWith` has been deprecated since 6.5 and is scheduled for removal in version 8. The replacement is the `scheduled()` creation function: `scheduled([1, 2, 3], asyncScheduler)` instead of `of(1, 2, 3, asyncScheduler)`. Time-based creation functions such as `interval` and `timer` keep their scheduler argument. ## The clock is part of the scheduler Each scheduler exposes `now()`, and time-based operators measure time with **their** scheduler's clock rather than reading the wall clock themselves. That is what makes schedulers swappable: pass a virtual-time scheduler to `debounceTime` or `delay` and those operators run against virtual time, with scheduled tasks executed synchronously in time order. The same design explains why `asapScheduler` and `queueScheduler` can fall back to `asyncScheduler` for positive delays: they extend its timer machinery and only change what happens at zero delay. ## When you would actually choose one - **`asapScheduler`**: to make a synchronous emission arrive asynchronously but before the next render or timer, without the delay of a timer. - **`queueScheduler`**: for recursive or iterative scheduling that should not grow the stack. - **`asyncScheduler`**: explicitly only when you need to override a default, for example to pass it to `observeOn`. - In tests, a virtual-time scheduler replaces all of them so timing can be asserted without waiting.

  • Why is queueScheduler described as a trampoline, and what problem does it solve?
    A task scheduled from inside a running queue task is not executed recursively; it is queued and run after the current task returns. Recursive rescheduling therefore becomes a flat loop instead of a deepening call stack, which prevents stack overflow in iterative or recursive scheduling.
  • Which scheduler do RxJS time-based operators use when you pass none?
    `asyncScheduler`. `interval`, `timer`, `delay`, `debounceTime`, `throttleTime`, `auditTime`, `sampleTime`, `bufferTime` and `timeout` all default to it, and accept a different scheduler as their last argument, which is how virtual-time tests or `animationFrameScheduler` are plugged in.

Think of three ways to answer a colleague's request: queueScheduler handles it now but finishes the current sentence before taking the next request, asapScheduler writes it at the top of today's to-do list, and asyncScheduler books a meeting for later.

saying these in an interview costs you the question

  • queueScheduler defers every task to a later tick, like setTimeout.
  • asapScheduler ignores any delay and always runs the task as a microtask.
  • asyncScheduler with zero delay runs before pending promise callbacks.
  • Without an explicit scheduler, of(1, 2, 3) emits asynchronously.
  • Passing a scheduler to of() is the recommended way to make it asynchronous in RxJS 7.