Task Queue

Manage Aurelia's scheduler and task utilities to coordinate asynchronous work, rendering, and tests.

Modern web applications juggle user input, network calls, and rendering. Aurelia's scheduler keeps that work predictable so you can focus on behavior instead of timing hacks.

To manage concurrency, Aurelia provides a task scheduling system. Think of it as an air-traffic controller that ensures every operation is processed in a predictable sequence.

Before you start: Familiarise yourself with Understanding the binding system so you know when bindings flush, and review App tasks if you plan to hook into startup or teardown.

It answers critical questions like:

  • Bulletproof Testing: How do you reliably test that a data change has updated the DOM? You stop guessing with setTimeout and instead wait for the exact moment all work is complete. This makes your tests fast, deterministic, and free of flaky failures.

  • Effortless Concurrency Control: How do you handle a user typing quickly in a search box without sending conflicting API requests? The scheduler gives you first-class tools to cancel outdated operations, effortlessly preventing race conditions.

  • Synchronized & Predictable Rendering: How do you perform an action right after Aurelia has painted a change to the screen? Because Aurelia's reactivity system is built on the same task queue, you have a reliable hook into the framework's lifecycle, ensuring your code runs at precisely the right time.

To see it in action, let's start with its most immediate and powerful use case: making your component tests 100% reliable.

Coming from Aurelia 1? The Aurelia 2 scheduler serves a similar purpose to the v1 TaskQueue but with a more powerful and explicit API. See our Migration Guide for specific details.

Getting started: reliable component tests

The single most common source of frustration in testing front-end applications is timing. You change a value, but the DOM doesn't update instantly. How long do you wait? If you've ever written a test that uses setTimeout to "wait for the UI to catch up," you've felt this pain.

The Aurelia scheduler completely eliminates this guesswork. Let's see how with a simple example.

The component under test

Imagine a basic counter component:

// counter.ts
export class Counter {
  count = 0;

  increment() {
    this.count++;
  }
}

The old, flaky way

Without a scheduler, you might write a test like this, using setTimeout with an arbitrary delay to wait for the DOM to update.

This test is fragile. It might pass on your fast machine but fail in a slow CI environment. What if the update takes 51ms? The test fails. What if it only takes 5ms? You've wasted 45ms. This is slow, unreliable, and a maintenance nightmare.

The Aurelia way: deterministic and reliable

With Aurelia, you don't guess. You tell the scheduler to wait until all queued work, including rendering, is finished.

TypeScript

That's it. The await tasksSettled() call pauses the test and resumes it only after Aurelia's scheduler has processed all pending tasks and updated the DOM.

Your tests are now:

  • Reliable: They no longer depend on arbitrary timers.

  • Fast: They wait for the minimum time required, not a millisecond more.

  • Clear: The intent of the test is immediately obvious.

This is the scheduler's core strength. Now, let's explore the concepts that make it possible.

Core Concepts

You've seen how tasksSettled() can make tests reliable, but how does it work? The Aurelia scheduler is built on a few key concepts that work together. Understanding them helps you control any asynchronous operation in your application.

tasksSettled(): The Key to Reliable Testing

While queueAsyncTask is for your application logic, tasksSettled() is its counterpart for testing. It is the primary tool for making your tests deterministic and reliable. It returns a promise that resolves only when the scheduler is completely idle.

"Idle" means that:

  1. The initial queue of tasks is empty.

  2. Any asynchronous operations started by those tasks (like promises returned from a callback) have also finished.

This is why it's so effective in tests. It doesn't just wait for one thing to finish; it waits for the entire chain reaction of updates and side effects to conclude. It resolves with true if any work was done, false if the scheduler was already idle, and rejects if any task threw an error.

queueAsyncTask(): Scheduling Controllable Work

queueAsyncTask() is your primary tool for adding a specific piece of work to the queue. It's designed for any operation that you might need to wait for, delay, or cancel.

You give it a callback function, and it returns a Task object, which is your handle to control that operation's lifecycle.

The Task handle

The Task object returned by queueAsyncTask() is your "receipt" for the scheduled work. It is a "thennable" object, meaning it behaves like a promise, but with additional properties for managing its lifecycle.

Directly await-able

The Task object can be awaited directly to get the result of the operation. Awaiting the task will return your callback's value once it completes, or throw an error if the callback rejects. This is the recommended and most common way to consume a task.

The task.status property

A property to inspect the task's current state ('pending', 'running', 'completed', or 'canceled').

The task.cancel() method

A method to abort the task before it has a chance to run.

The task.result property

For advanced use cases, the underlying Promise is accessible via .result. This can be useful when interoperating with libraries that require a native promise instance. In most situations, you should await the Task object directly.

queueRecurringTask(): repeating actions

For actions that need to repeat on a timer, like polling a server for live notifications, queueRecurringTask() is the right tool. It runs your callback on a given interval until you explicitly cancel it. It returns a special RecurringTask handle that lets you stop the loop with .cancel() or wait for the next tick with await task.next(), which is very useful for testing.

A Note on queueTask()

You may also see the simpler queueTask(). This is a lower-level "fire-and-forget" function. It does not return a Task handle and cannot be awaited or canceled directly. It's primarily used internally by the framework and for niche plugin scenarios. For application code, queueAsyncTask() is almost always the correct choice.

Practical recipes

Now that you understand the core concepts, let's see how to combine them to solve common development problems. Each recipe here provides a practical, copy-paste-friendly solution you can adapt for your own applications.

UI & Animation

Recipe: Creating a Delayed Hover Tooltip

Problem: You want to show a tooltip, but only if the user intentionally hovers over an element for a moment. If they just quickly pass their mouse over it, the tooltip should not appear.

Solution: Use queueAsyncTask with a delay to schedule the tooltip's appearance. If the user's mouse leaves the element before the delay is over, cancel() the pending task.

Recipe: Preventing Form Double-Submission

Problem: A user clicks a "Save" button that triggers a network request. If the request is slow, they might click the button again, sending a duplicate request.

Solution: Disable the button as soon as it's clicked. Use queueAsyncTask to perform the save operation and re-enable the button in a finally block. This guarantees the button becomes interactive again, even if the save operation fails.

Recipe: Building an Auto-Dismissing Notification

Problem: You need to show a "toast" notification that automatically disappears after a few seconds.

Solution: Use queueAsyncTask with a delay. This is a classic "fire-and-forget" scenario. You schedule a task to hide the notification and don't need to manage it further.

Data & Concurrency

Recipe: Managing Component Loading States

Problem: You need to show a loading spinner while data is being fetched and ensure it's hidden when the operation is complete, even if the fetch fails.

Solution: Use queueAsyncTask to wrap your fetch logic. A try...finally block provides a rock-solid way to guarantee your isLoading flag is set back to false, regardless of the outcome.

Recipe: Cancelling Outdated Data Fetches

Problem: A component displays data based on a filter. If the user changes the filter quickly, a slow, old request might finish after a newer one, overwriting fresh data with stale data.

Solution: Store the handle to the current fetch task. When a new fetch is initiated, cancel() the previous one before you begin. This ensures only the results from the very last request will be processed.

Timers & Periodic Tasks

Recipe: Polling a Backend for Live Updates

Problem: You need to periodically fetch fresh data from a server to create a "live" experience, like a dashboard, a news feed, or a stock ticker. Managing this with setInterval can be messy and lead to memory leaks if not cleaned up properly.

Solution: Use queueRecurringTask to create a managed, repeating task. It integrates with Aurelia's scheduler and can be easily started and stopped within your component's lifecycle hooks, making it both powerful and safe.

This pattern is also highly testable. In a test environment, you can await poller.next() to precisely synchronize your assertions with each polling cycle, eliminating the need for fragile setTimeout waits.

Testing a Recurring Task

Here's how you could write a reliable test for the LiveScores component, using await poller.next() to control the flow of time. Notice how there are no setTimeout hacks.

Advanced Topics & Best Practices

This section covers specialized functions and patterns for niche scenarios. The tools here are powerful but should be used with care, as they are intended for framework authors, plugin developers, or solving complex integration challenges. For most application development, the recipes in the previous sections are all you'll need.

When to Use runTasks() Synchronously (In Tests)

Problem: You're writing a test for a low-level component where the interaction is fundamentally synchronous. The action queues a microtask (like a render update), but you want to keep your test function simple and synchronous without needing async/await

Solution: Call runTasks() to synchronously "flush" the scheduler's queue immediately after your action. This is the non-async equivalent of await tasksSettled() that allows you to make your assertions directly in a non-async test.

Ensuring Clean Tests

Cleaning Up All Recurring Tasks

Problem: In a large test suite, a RecurringTask from one test might not be properly cancelled. It can "leak" into subsequent tests, causing unpredictable behavior and failures that are hard to debug.

Solution: Use getRecurringTasks() in a global afterEach hook in your test setup. This ensures that after every single test, any lingering recurring tasks are found and cancelled.

Detecting Leaked Microtasks with tasksSettled

Problem: A test might start a fire-and-forget async operation but not await its completion. This "leaked" work from one test could potentially interfere with the setup or execution of the next test.

Solution: Use the boolean return value of await tasksSettled() in an afterEach hook. If it returns true, it means the test that just finished left pending work on the scheduler. You can then fail the test explicitly, forcing you to find and properly await the orphaned task.

Plugin Authoring: Using queueTask

Problem: You are authoring a plugin, like a custom binding behavior, and need to perform a low-level DOM manipulation that must be perfectly synchronized with Aurelia's own rendering life cycle.

Solution: Use queueTask(). This places your synchronous, fire-and-forget function into the very same microtask queue that Aurelia uses for its bindings. This guarantees your code runs with, not against, the framework's update cycle.

Advanced Component Logic: Synchronous Flushing with runTasks

Problem: You need to interact with a third-party, non-async library that synchronously reads a DOM property immediately after you've changed the state that controls it.

Solution: Call runTasks() as an escape hatch to force Aurelia's rendering to complete within the same function call.

Next steps

Last updated

Was this helpful?