Introduction
Un-awaited expect(...).resolves in test suites is a silent saboteur of test reliability. When a test uses expect(promise).resolves without await, the test runner completes before the assertion evaluates. This mechanical process—the test lifecycle ending prematurely—allows the test to pass regardless of the promise’s outcome. The observable effect? False positives that mask failures, eroding trust in test results and increasing the risk of shipping broken code.
This issue is pervasive. An analysis of 44 popular open-source repositories revealed 36 instances of un-awaited .resolves, with one project alone containing 41 such tests. These tests, superficially correct, slipped through code reviews, highlighting a systemic gap between awareness and enforcement. The root causes include:
-
Lack of awareness: Developers often misunderstand the async nature of
.resolves, assuming it blocks the test runner. - Insufficient linting: Default ESLint or Vitest rules rarely catch all forms of un-awaited promises, especially in complex test helpers.
- Code review fatigue: Relying solely on manual review fails at scale, as subtle async issues are easily overlooked.
The stakes are high. With automated testing as the backbone of modern CI/CD pipelines, false positives directly translate to undetected bugs in production. This article dissects the problem, evaluates common solutions—from linting rules to assertion guards—and identifies the most effective strategies based on real-world team experiences. By the end, you’ll have a clear rule for preventing un-awaited promises in your test suites.
Common Scenarios and Solutions
Un-awaited expect(...).resolves is a silent killer of test reliability. Below are six common scenarios where this issue arises, along with actionable solutions and best practices to prevent false positives. Each scenario is grounded in real-world observations and technical mechanisms.
- Scenario 1: Direct Un-awaited
.resolvesin Async Tests
Example: expect(save(user)).resolves.toBe(true) without await.
Mechanism: The test runner completes before the promise resolves, bypassing the assertion. The test passes regardless of the promise outcome, masking failures.
Solution: Use await with .resolves. Enforce this via eslint-plugin-jest's valid-expect rule with alwaysAwait. This rule catches un-awaited .resolves and .rejects but may miss complex helpers. Optimal for: Teams using Jest with ESLint. Failure condition: Custom helpers wrapping expect may evade detection.
- Scenario 2: Un-awaited Promises in TypeScript Tests
Example: expect(asyncOperation()).resolves.toBe(true) in a TypeScript file.
Mechanism: TypeScript’s type system doesn’t enforce awaiting promises by default, allowing tests to slip through.
Solution: Enable @typescript-eslint/no-floating-promises with type-aware linting. This rule catches un-awaited promises but slows linting. Optimal for: TypeScript-heavy teams willing to trade linting speed for thoroughness. Failure condition: Disabled type-aware linting or non-TypeScript files.
- Scenario 3: Complex Helpers Wrapping Un-awaited Promises
Example: expect(myHelper(user)).resolves.toBe(true), where myHelper returns a promise.
Mechanism: Linting rules often fail to trace promises through function calls, allowing un-awaited promises to slip through.
Solution: Combine eslint-plugin-jest with manual code review for complex helpers. Optimal for: Teams with moderate test complexity. Failure condition: Overreliance on linting without review leads to missed cases.
- Scenario 4: Missing Assertion Guards in Async Tests
Example: Async test without expect.assertions(n) or expect.hasAssertions().
Mechanism: Tests without assertion guards can pass even if no assertions are evaluated, hiding un-awaited promises.
Solution: Enforce expect.assertions(n) in all async tests. This ensures assertions are present but doesn’t prevent un-awaited promises. Optimal for: Teams needing a quick win. Failure condition: Assertions present but un-awaited, leading to false positives.
- Scenario 5: Overreliance on Code Review
Example: Tests reviewed but un-awaited promises still slip through.
Mechanism: Code review fatigue and the subtlety of async issues lead to oversight at scale.
Solution: Pair code review with automated linting rules. Optimal for: All teams. Failure condition: Disabled linting or insufficient reviewer training on async patterns.
- Scenario 6: Vitest-Specific Un-awaited Promises
Example: expect(save(user)).resolves.toBe(true) in a Vitest suite.
Mechanism: Vitest’s default rules may not catch all un-awaited promises, especially in complex setups.
Solution: Enable Vitest’s built-in linting rules and supplement with eslint-plugin-vitest. Optimal for: Vitest users. Failure condition: Misconfigured rules or non-standard test patterns.
Best Practices to Prevent False Positives
-
Rule 1: If using Jest, enable
eslint-plugin-jestwithalwaysAwaitand pair with code review for complex helpers. -
Rule 2: For TypeScript, enable
@typescript-eslint/no-floating-promiseswith type-aware linting, accepting the performance trade-off. -
Rule 3: Enforce
expect.assertions(n)in async tests as a secondary guard, but don’t rely on it alone. - Rule 4: Avoid overreliance on code review; automate checks wherever possible.
By addressing these scenarios with the right combination of tools and practices, teams can eliminate un-awaited promises, restore trust in test results, and reduce the risk of production bugs.
Conclusion and Recommendations
Un-awaited expect(...).resolves in test suites is a pervasive issue that undermines test reliability by allowing tests to pass before assertions are evaluated. Our analysis of 44 open-source repositories revealed 36 instances of this problem, with one project alone containing 41 un-awaited .resolves tests. The root causes include lack of awareness about the async nature of .resolves, insufficient linting, and overreliance on code review. Left unaddressed, this issue erodes trust in test results and increases the risk of shipping broken code.
Key Recommendations
-
Jest Teams: Enable
eslint-plugin-jestwith thealwaysAwaitoption in thevalid-expectrule. This catches un-awaited.resolvesand.rejectsdirectly. However, it may miss promises wrapped in custom helpers. Pair this with targeted code review for complex test setups. -
TypeScript Teams: Enable
@typescript-eslint/no-floating-promiseswith type-aware linting. This rule catches un-awaited promises in TypeScript files but comes with a performance trade-off due to slower linting. Accept this trade-off for thoroughness. -
Vitest Users: Leverage Vitest’s built-in linting rules and supplement with
eslint-plugin-vitest. Ensure rules are properly configured to catch un-awaited promises in Vitest-specific patterns. - All Teams: Avoid overreliance on code review alone. Automate checks wherever possible, as manual reviews often miss subtle async issues at scale.
Secondary Guards
For additional safety, enforce expect.assertions(n) in async tests to ensure assertions are present and evaluated. While this doesn’t catch un-awaited promises directly, it acts as a fail-safe against tests passing without assertions.
Decision Rule
If your team uses Jest and ESLint, enable eslint-plugin-jest with alwaysAwait and pair it with code review for complex helpers. If you’re in a TypeScript-heavy environment, prioritize @typescript-eslint/no-floating-promises with type-aware linting, accepting the performance hit for thoroughness. For Vitest users, rely on Vitest’s built-in rules and supplement with eslint-plugin-vitest.
Failure Conditions
-
Jest: Custom helpers wrapping
expectmay evade detection. Manually review these cases. - TypeScript: Disabled type-aware linting or non-TypeScript files will leave gaps. Ensure consistent configuration.
- Vitest: Misconfigured rules or non-standard test patterns can lead to missed un-awaited promises. Validate rule coverage.
By adopting these strategies, teams can eliminate un-awaited promises, restore test reliability, and reduce the risk of production bugs. The choice of solution depends on your tech stack and tolerance for linting performance trade-offs, but automation paired with targeted review is the most effective approach across all scenarios.
Top comments (0)