Choosing Between Jasmine's done Callback and async/await for Async Tests
Learn when to use Jasmine's done callback versus async/await to properly test asynchronous code and avoid false positives or timeout errors.
07 Sept 2026, 14:56 UTC

The Async Timeout Trap
A common frustration when writing tests for asynchronous JavaScript is the "false positive." You write a test for a function that fetches data or triggers a timer, the test runs instantly, and Jasmine reports a pass—even though your assertions never actually executed. This happens because Jasmine finishes the spec as soon as the function call is initiated, not when the internal promise resolves or the timer fires.
The Explicit Signal: Using the done Callback
The done callback is the traditional way to handle asynchronicity in Jasmine. When you include done as an argument in your it block, Jasmine pauses the spec execution. It will not move to the next test until the done() function is invoked.
This is particularly useful for testing legacy callback-based APIs or complex event sequences where a promise isn't naturally returned.
Risks of the done Pattern
- The Forgotten Call: If you forget to call
done(), the test will hang until it hits the default timeout (usually 5 seconds), resulting in a failure that doesn't point to a specific assertion error. - Error Handling: If an assertion fails inside an asynchronous callback, the error might be thrown outside the scope of the test runner, potentially crashing the process or leaving the test in a permanent pending state.
The Modern Approach: Async/Await and Promises
Since version 3.5, Jasmine supports returning a promise from the it function. By marking the spec function as async, you can use await to pause execution in a way that looks synchronous but behaves asynchronously.
Jasmine detects the returned promise and automatically waits for it to settle. If the promise rejects, the spec fails. This eliminates the need for a manual done() call and makes the code significantly more readable.
Worked Example: Testing a Delayed Greeting
Consider a function delayedGreeting that returns a string after 10 milliseconds. Here is how to test it using both methods.
// The function to test
const delayedGreeting = (name) => {
return new Promise((resolve) => {
setTimeout(() => resolve(`Hello, ${name}!`), 10);
});
};
describe('Greeting Service', () => {
// Method 1: Using the done callback
it('should greet the user (done callback)', (done) => {
delayedGreeting('Alice').then((res) => {
expect(res).toBe('Hello, Alice!');
done(); // Signal completion
}).catch(done.fail); // Pass errors to Jasmine
});
// Method 2: Using async/await (Jasmine 3.5+)
it('should greet the user (async/await)', async () => {
const res = await delayedGreeting('Bob');
expect(res).toBe('Hello, Bob!');
});
});
Execution and Verification
To run this spec, ensure you have Jasmine installed and run the following command in your terminal:
# Run via the Jasmine CLI
jasmine example.spec.js
Permissions: No special permissions are required beyond standard node execution rights.
Expected Result: Two passing specs.
Verification: To verify the timeout behavior, remove the done() call from the first test. You should see a failure stating "Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL".
Trade-offs and Limitations
While async/await is generally preferred, there are specific engineering trade-offs to consider:
| Feature | done() Callback | async/await |
|---|---|---|
| Readability | Nested (Callback Hell) | Linear/Flat |
| Version Req. | All versions | Jasmine 3.5+ |
| Control | High (manual signal) | Automatic (promise-based) |
Warning on Mock Clocks: When testing timers (like setTimeout), you may use jasmine.clock().install() to fast-forward time. If you do this, remember to call jasmine.clock().uninstall() in an afterEach block. Failing to uninstall the clock leaks state into other tests, causing non-deterministic "flaky" failures.
Final Decision Guide
Choose async/await for 90% of your needs, especially when dealing with Promises or API calls. Reserve the done callback for event-driven code or legacy libraries that do not provide a Promise-based interface. Avoid mixing both styles within a single it block, as this can lead to confusing execution orders and redundant timeout errors.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.