Jest finishing async test before done
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
A common Jest problem is tests that appear to pass while async work has not finished. This happens when the test function does not return a Promise, does not await, or incorrectly uses done together with Promise-based code. The result is false positives: CI is green, but behavior is untested.
Reliable async testing in Jest requires one clear completion mechanism per test. Either return/await a Promise, or use done for callback-style APIs. Mixing patterns is where most mistakes start.
Core Sections
1. Promise-based test must return or await
Equivalent Promise-return style:
2. Use done only for callback APIs
Call done(err) on failure paths, otherwise Jest may hang or misreport.
3. Avoid mixing done with async/await
Anti-pattern:
Pick one model. For Promise-based code, drop done.
4. Assert async failures correctly
Without await, rejection assertions may not run before test ends.
5. Use fake timers carefully
When timers are mocked, advance timers before asserting results.
Timer state leakage between tests can also create flaky async behavior.
6. Add expect.assertions for callback safety
This catches tests that exit before expected assertions run.
Common Pitfalls
- Forgetting to return or await a Promise in an async test.
- Mixing
donewith Promise/async syntax in one test body. - Not wiring error paths to
done(err)in callback tests. - Using rejection assertions without
await. - Leaving fake timer state unreset between tests.
Summary
Jest finishing async tests too early is usually a completion-signaling bug. Use one pattern per test: return/await Promise or callback done, not both. Add explicit rejection assertions, timer control, and assertion counts where appropriate. With consistent async test style, false positives disappear and test results become trustworthy.
For teams maintaining jest finishing async test before done in long-lived codebases, reliability improves when implementation guidance is paired with a lightweight verification routine. A practical pattern is to define three test categories up front. First, happy-path tests that validate normal expected inputs. Second, boundary tests that include empty values, minimum and maximum limits, and malformed records from real logs. Third, operational tests that simulate production-like behavior under retries, parallel execution, and partial failure. This combination catches both obvious logic defects and the subtle integration issues that usually appear after deployment.
It is also useful to encode assumptions close to the code rather than leaving them in scattered documentation. Add short comments where invariants matter, keep helper utilities centralized, and avoid repeating slightly different logic in multiple modules. In CI, run a small deterministic suite on every commit and a broader dataset suite on schedule. When incidents occur, convert the failing scenario into a permanent regression test before patching. Over time this creates a strong feedback loop where jest finishing async test before done behavior remains stable even as dependencies, framework versions, and team ownership change. The result is less firefighting and faster review cycles. A single flaky-test dashboard with trend history also helps teams spot async reliability regressions early.

