Jasmine Async Testing: From done() Callbacks to async/await — What Changed and What Didn't
Jasmine's async testing evolved from done() callbacks to native async/await. This post covers version-specific behavior, a practical migration pattern using promisify, clock mocking differences, and the readability vs. debugging trade-off.
13 Jan 2026, 09:34 UTC

The Problem: Async Tests That Lie
You've seen it: a test passes locally but fails in CI, or worse — passes when it should fail because the assertion never ran. In Jasmine, this used to be routine. The done() callback pattern forced you to manually signal completion, and forgetting to call it (or calling it twice) turned flaky tests into a debugging sport.
Jasmine 3.0+ introduced native Promise support, and 3.5+ stabilized async/await. The syntax is cleaner, but the mental model shifted. This post walks through what actually changed, where the old pattern still matters, and a migration pattern that keeps stack traces useful.
From done() to Promises: The Version Timeline
- Jasmine 2.0 (2014) —
done()callback required. Default timeout: 5 seconds (jasmine.DEFAULT_TIMEOUT_INTERVAL). - Jasmine 3.0 (2017) — Return a Promise from
it()and Jasmine waits for resolution/rejection. Nodone()needed. - Jasmine 3.5 (2019) —
async/awaitworks identically to returning a Promise. Stack traces preserved in modern Node and browsers.
The done() callback never went away. It's still the only way to test non-Promise async APIs — EventEmitter, bare setTimeout, or legacy callback libraries — without wrapping them first.
Worked Example: Migrating a setTimeout Test
Suppose you have a debounce utility that uses setTimeout. Here's the same spec in both styles.
Legacy done() style
it('calls callback after delay', function(done) {
const fn = jasmine.createSpy('callback');
debounce(fn, 50);
fn(); // trigger
setTimeout(function() {
expect(fn).toHaveBeenCalled();
done();
}, 100);
});
Modern async/await style (with Promise wrapper)
const { promisify } = require('util');
const setTimeoutPromise = promisify(setTimeout);
it('calls callback after delay', async function() {
const fn = jasmine.createSpy('callback');
debounce(fn, 50);
fn();
await setTimeoutPromise(100);
expect(fn).toHaveBeenCalled();
});
The wrapper is a one-liner using util.promisify (Node) or a tiny helper in the browser. After that, every spec in the suite uses await and reads synchronously.
Where async/await Can Mislead
Two traps caught teams upgrading from 3.x to 3.5+:
- Pre-await throws — In Jasmine < 3.5, an
Errorthrown before the firstawaitin anasyncspec might not fail the test. The fix: upgrade, or wrap the whole body inPromise.resolve().then(() => { ... }). - Mixing styles — Using
done()and returning a Promise in the sameit()causes undefined behavior. Pick one per spec.
Spy return values need attention too: spyOn(obj, 'method').and.returnValue(Promise.resolve(42)) works with await, but returning a plain value breaks the await chain.
Clock Mocking: Different Mental Models
jasmine.clock().install() works with both patterns, but the microtask timing differs.
- done() — You control
tick()manually; the callback runs when you advance. - async/await — You must flush microtasks first:
await Promise.resolve()beforejasmine.clock().tick(100), otherwise the Promise continuation hasn't queued yet.
it('advances clock with async/await', async function() {
jasmine.clock().install();
const fn = jasmine.createSpy('callback');
setTimeout(fn, 100);
await Promise.resolve(); // flush microtasks
jasmine.clock().tick(100);
expect(fn).toHaveBeenCalled();
jasmine.clock().uninstall();
});
Trade-off: Readability vs. Debugging Fidelity
async/await wins on readability — specs look like synchronous code. But stack traces can be shorter when a rejection bubbles through multiple await points. The done() style, verbose as it is, gives you an explicit call site in the callback. For complex async flows (e.g., streaming, retries), some teams keep done() for the final assertion to preserve the full trace.
Parallel runners (jasmine-parallel, etc.) also handle async/await more predictably; done() callback ordering isn't guaranteed across workers.
Actionable Checklist for Your Suite
- Run
npx jasmine --version. If < 3.5, plan an upgrade before migrating syntax. - Identify specs using
done()with Promise-returning APIs — those are the easiest to convert. - Create a
test-helpers.jswith promisified versions ofsetTimeout,fscallbacks, or your legacy library. - Convert one file, run the suite, and verify stack traces by throwing inside an
asyncspec. - Adjust timeouts per-spec where needed:
it('...', async () => {}, 10000).
The migration doesn't have to be all-or-nothing. Keep done() for the few specs that need it; use async/await everywhere else. Your CI will thank you.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.