What does the browser's scheduler.postTask() let you express about a piece of work that setTimeout and requestIdleCallback cannot, and what priorities does it accept?
answer
- setTimeout has exactly one dial
- three named levels, no numbers
- it hands back a promise, not a handle
- a controller can change its mind later
- postTask, TaskController, setPriority
basics
~20 sscheduler.postTask() schedules a task at an explicit priority — 'user-blocking', 'user-visible' (the default), or 'background' — returns a promise for its result, and accepts a signal so the task can be cancelled or re-prioritised. setTimeout offers only a delay; requestIdleCallback offers only "whenever there is room".
solid answer
~50 s`setTimeout` gives you one dial, a delay, and every timer callback competes on equal terms; `requestIdleCallback` gives you the opposite extreme, work that runs only in leftover time. `scheduler.postTask(callback, options)` fills the gap by making priority a first-class argument. It accepts `priority` — `'user-blocking'` for work the user is waiting on, `'user-visible'` (the default) for ordinary visible work, and `'background'` for work nobody is waiting on — and the browser runs higher-priority tasks first regardless of the order you posted them. It also accepts `delay`, so it subsumes `setTimeout`, and a `signal`, which is either an `AbortSignal` for cancellation or a `TaskSignal` from a `TaskController` whose `setPriority()` can promote or demote a task that has not run yet. `postTask` returns a promise that resolves with the callback's return value and rejects if the task is aborted, so scheduled work composes with `await` and with normal error handling.
code
javascript · 11 linesconst controller = new TaskController({ priority: 'background' });
const done = scheduler
.postTask(() => 'summary ready', { signal: controller.signal })
.then((value) => console.log(value))
.catch((error) => {
if (error.name !== 'AbortError') throw error;
});
// the user scrolled the panel into view before the task ran
controller.setPriority('user-blocking');go deeper
Know that scheduler.postTask schedules a callback with an explicit priority — user-blocking, user-visible, or background — and returns a promise, unlike setTimeout which only takes a delay.
Be ready to compare the three primitives on what each can express, and to show cancellation with an AbortSignal and promotion with a TaskController's setPriority().
Expect to point out that priority orders pending tasks and never preempts a running one, so labelling expensive work as background changes nothing about the block it causes when it finally runs.
Own how priorities are assigned across an app so that the labels keep meaning something, and decide where the feature detection and fallback live rather than letting each team invent one.
## The gap in the older primitives Before this API, the platform gave a page exactly two ways to defer main-thread work, and neither expressed intent. `setTimeout(fn, 0)` says "soon, in a fresh task", and every caller who says it lands in the same undifferentiated queue: a prefetch scheduled that way is indistinguishable from the work that repaints the thing the user just clicked. `requestIdleCallback(fn)` says "only when there is nothing else", which is correct for genuinely disposable work and useless for anything that matters within a bounded time. There was no way to say "important, but not now" or "do this only after the visible work". `scheduler.postTask()` is the platform's answer. The `scheduler` object is a global (available on `window` and inside workers), and the call looks like this: ```js const result = await scheduler.postTask(() => computeSummary(rows), { priority: 'background', }); ``` ## The three priorities - **`'user-blocking'`** — the user is blocked on this. Rendering a response to a click, or work that must finish before the next paint is meaningful. Highest priority; use it sparingly, because everything marked urgent means nothing is. - **`'user-visible'`** — the default when you pass no priority. Work whose result the user will see but is not actively waiting on: populating a secondary panel, updating a count. - **`'background'`** — nobody is waiting. Prefetching, log serialisation, warming a cache. These are the only three; there is no numeric scale. The browser interleaves them with its own work, so a `'background'` task still runs ahead of an idle callback but behind rendering and input. ## Cancellation and re-prioritisation The `signal` option accepts an `AbortSignal`, so scheduled work cancels through the same mechanism as `fetch`: ```js const ac = new AbortController(); scheduler.postTask(work, { signal: ac.signal }).catch((e) => { if (e.name === 'AbortError') return; // expected throw e; }); ac.abort(); ``` For priority changes there is `TaskController`, whose `signal` is a `TaskSignal`: ```js const tc = new TaskController({ priority: 'background' }); scheduler.postTask(render, { signal: tc.signal }); tc.setPriority('user-blocking'); // the user just scrolled it into view ``` That pattern — post at a low priority, promote when the user's attention arrives — is the thing neither older primitive can express at all. With `setTimeout` your only recourse is to cancel and re-schedule, which loses the task's place and its promise. ## The promise return value `postTask` returns a promise that fulfils with whatever the callback returns (a returned promise is awaited) and rejects with the abort reason if the signal fires, or with whatever the callback throws. This is a real ergonomic difference from `setTimeout`, where a throw inside the callback surfaces as an unhandled error on the global with no way to attach a handler at the call site. ## What it does not do A posted task is still a task on the main thread and still runs to completion. Marking work `'background'` does not make it interruptible, and a 300 ms background task blocks input for 300 ms exactly like any other. Priority controls *ordering among pending tasks*, never preemption of a running one. Work that is genuinely expensive belongs in chunks — or off the main thread entirely. ## Availability As of 2026 `scheduler.postTask` is available in Chromium-based browsers and in Firefox; Safari support is more recent, so production code feature-detects (`'scheduler' in globalThis && 'postTask' in scheduler`) and falls back to a timer-based shim. `TaskController` ships alongside `postTask` in the engines that implement it. Because the fallback path is trivial to get subtly wrong — priority silently collapses to a single queue — most codebases centralise the detection in one small module rather than scattering feature checks across call sites.
- If scheduler.postTask accepts a delay option, is there any reason left to use setTimeout?Portability and habit, mostly. `postTask` with `{ delay }` behaves like a timer that also carries a priority and a promise, so functionally it supersedes `setTimeout` in the engines that implement it. But `setTimeout` is universally available and needs no feature detection, so most code keeps it for trivial delays and reaches for `postTask` when the priority or the signal is the point.
- Does marking work as 'background' priority make it safe to run something expensive?No. Priority decides which pending task the browser picks next; it does not make a running task interruptible. A 300 ms background callback blocks input for 300 ms just as a user-blocking one would. The only safe treatment for expensive work is to split it into small units that yield between them, or to move it off the main thread entirely.
- What is the difference between passing an AbortSignal and passing a TaskSignal from a TaskController?An `AbortSignal` only cancels: abort it and the task never runs, and its promise rejects with an AbortError. A `TaskSignal`, obtained from `new TaskController({ priority })`, additionally carries the task's priority and lets you call `setPriority()` to promote or demote work that has not run yet — the pattern for posting speculatively at background priority and raising it when the user actually needs the result.
- How would you feature-detect this API without changing the shape of your calling code?Wrap it once. Export a single `postTask(fn, options)` helper that uses `scheduler.postTask` when `'scheduler' in globalThis && 'postTask' in globalThis.scheduler`, and otherwise returns a promise around `setTimeout`, ignoring priority. Call sites stay declarative, and the day support becomes universal you delete one branch instead of auditing every scheduling site.
saying these in an interview costs you the question
- Invents numeric priority levels instead of the three named ones
- Thinks a background task can be preempted mid-execution
- Believes postTask moves work off the main thread
- Confuses the priority option with requestIdleCallback's timeout
- Assumes it is available everywhere without a fallback