Diagnosing Jasmine done() Timeout Failures in Asynchronous Specs
A step‑by‑step guide to identify why a Jasmine spec that uses setTimeout, Promise, or AJAX hangs or times out, and how to fix the done() callback usage.
07 Jul 2026, 06:39 UTC

Recognizable condition
When a Jasmine spec that contains asynchronous code (e.g., setTimeout, Promise, XHR/fetch) finishes with a timeout error like Error: Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL or appears to hang indefinitely, the most common cause is that the done callback was not invoked exactly once.
Cause / diagnostic table
| Observed symptom | Likely cause |
|---|---|
| Spec times out after default interval | done() never called (missing in success or error path) |
| Spec fails with "done() called more than once" | done() invoked in both success and error handlers, or inside a finally block without guarding |
| Spec passes but assertions run too early | done() called before the asynchronous operation resolves |
Spec hangs when using async/await together with done() | Mixed styles cause Jasmine to wait for both a promise and done() |
Ordered checks
- Verify the spec function signature – Open the spec file and confirm the
it(ordescribe) callback declares a parameter, typically nameddone:
If the parameter is missing, Jasmine treats the spec as synchronous and will timeout on any async work.it('should fetch data', function(done) { // async work }); - Trace every asynchronous path – For each async API used (e.g.,
setTimeout,Promise.then,xhr.onreadystatechange), ensure there is exactly one call todone()in the success handler and, if error handling is needed, one call in the error handler. Look for stray calls infinallyblocks or outside the async callbacks. - Confirm placement after resolution – The
done()call must appear after the async operation has settled. For aPromise, place it inside.thenand.catch(orfinallywith a guard). For timer‑based code, calldone()after the timeout elapses. - Check for duplicate invocations – Search the spec for more than one occurrence of
done()that could be reached on the same execution path. A common duplicate is callingdone()both in.thenand in afinallyblock that runs regardless of outcome.
Fixes tied to findings
Missing done()
Add the callback in every path that finishes the async work:
it('resolves a promise', function(done) {
fetch('/api/data')
.then(response => response.json())
.then(data => {
expect(data.value).toBe(42);
done();
})
.catch(err => {
fail(err); // optional: mark spec as failed
done();
});
});
Done() called multiple times
Wrap the call in a guard or use a flag to ensure it runs once:
it('handles success and error', function(done) {
let called = false;
const finish = () => {
if (called) return;
called = true;
done();
};
someAsyncOp()
.then(result => {
expect(result).toBeTruthy();
finish();
})
.catch(err => {
fail(err);
finish();
});
});
Done() before async completes
Move the call inside the resolution handler, not before the async invocation:
// WRONG
it('wrong placement', function(done) {
done(); // too early
setTimeout(() => { /* ... */ }, 100);
});
// CORRECT
it('correct placement', function(done) {
setTimeout(() => {
expect(true).toBeTrue();
done();
}, 100);
});
Prefer returning a promise (Jasmine 2.0+)
If you are on Jasmine ≥2.0, you can avoid done() entirely by returning a promise:
it('returns a promise', function() {
return fetch('/api/data')
.then(r => r.json())
.then(data => {
expect(data.id).toBeDefined();
});
});
This style eliminates the risk of missing or duplicate done() calls.
Using Jasmine clock for timer‑based code
For specs that rely on setTimeout or setInterval, install the mock clock:
beforeEach(function() {
jasmine.clock().install();
});
afterEach(function() {
jasmine.clock().uninstall();
});
it('advances timer', function(done) {
setTimeout(() => {
expect(true).toBeTrue();
done();
}, 200);
jasmine.clock().tick(201); // moves time forward
});
Escalation criteria
- If after applying the above fixes the spec still times out, verify that any external mocks (e.g.,
fetch-mock,axios-mock-adapter) are correctly resolving or rejecting. - Check the version of Jasmine in use (
jasmine --version). Versions prior to 2.0 have limited promise support; upgrading to a recent release (≥3.6) improves async handling. - When the spec involves complex asynchronous coordination (multiple promises, race conditions), consider refactoring the test to use
async/awaitwith a returned promise, or split the spec into smaller, focused units. - If the problem persists despite correct
done()usage and upgraded Jasmine, flag the test for review by the lead engineer or the team’s testing champion.
Practical verification
Run the spec with Jasmine’s verbose flag to see the execution flow:
# Run from the project root jasmine spec/myAsyncSpec.js --verboseLook for output lines like:
Spec ... passed– indicates the spec completed without timeout.Spec ... failed: Error: Timeout - Async callback was not invoked– points to a missingdone().Spec ... failed: Error: done() called more than once– indicates duplicate invocation.
As a quick sanity check, temporarily replace the async call with a synchronous stub:
it('stubbed version', function(done) { // replace setTimeout / fetch with immediate value expect(true).toBeTrue(); done(); });If the stubbed spec passes instantly, the original timeout is due to the async handling, not the assertions.
Limitations
This guide assumes the spec uses Jasmine’s built‑in
done mechanism. If you are using third‑party async matchers (e.g.,jasmine‑promise) or a custom test runner, the exact steps may differ. Always consult the documentation of the specific matcher or runner for equivalent verification methods.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.