Diagnosing Mocha Async Test Failures: Hanging Tests, Uncaught Rejections, and Timeout Issues
A step‑by‑step diagnostic guide for Mocha async test failures: hanging tests, uncaught promise rejections, timeout misconfiguration, and parallel‑mode race conditions.
03 Oct 2026, 17:05 UTC

Recognizable condition
When running a Mocha test suite you observe one or more of the following symptoms:
- Tests appear to pass but the Node process never exits (hanging test).
- Mocha reports a passing test while Node prints an "UncaughtPromiseRejectionWarning".
- A test fails with a timeout error even though the asynchronous work finishes quickly.
- Tests are flaky only when run with
--parallel.
Cause and diagnostic table
| Symptom | Typical cause | Quick diagnostic check |
|---|---|---|
| Process hangs after test run | Async test never settles (no done(), no returned/resolved Promise) and Mocha lacks --exit or pending handles. | Run the suite with --exit. If the process still hangs, look for timers/intervals or unclosed resources. |
| Passing test + uncaught rejection warning | A rejected Promise is created inside the test but not returned/awaited, so Mocha cannot associate the failure. | Add a return or await to the Promise and re‑run; the warning should disappear and the test should fail. |
| Timeout error despite fast code | this.timeout() called inside an arrow function (lexical this) or global timeout too low. | Replace arrow function with a regular function or set timeout via mocha.opts/.mocharc.json and re‑run. |
Flaky failures only with --parallel | Shared mutable state (global vars, singletons, DB connections) causes race conditions. | Run the same suite with --jobs=1. If flakiness disappears, the issue is state isolation. |
Ordered checks
- Verify Mocha and Node versions
Run
npx mocha --versionandnode --version. This guide assumes Mocha ≥ 8 and Node ≥ 14 (Mocha 10+ drops Node < 14). - Isolate a single test
Create a minimal test file
test/example.jscontaining only the suspect test. This removes interference from other suites. - Check for hanging
Execute
npx mocha test/example.js --exit. Observe:- If the process exits with a non‑zero code and a timeout error, the test likely never settled.
- If the process stays alive, look for lingering timers (
setInterval) or open handles (e.g., server not closed).
- Detect uncaught rejections
Run without
--exitand watch the console. If you see a warning but the test reportsPASS, the test is swallowing a rejection. - Validate timeout configuration
Inspect the test or hook for
this.timeout(). If it resides inside an arrow function, replace it:// ❌ Arrow function – timeout ignored it('should work', () => { this.timeout(5000); // ineffective return doAsyncThing(); }); // ✅ Regular function – timeout honored it('should work', function () { this.timeout(5000); return doAsyncThing(); });Re‑run the test and confirm the timeout behaves as expected.
- Check for shared state in parallel mode
Run the suite with
--parallel --jobs=1. If failures disappear, examine globals, singletons, or database connections that are not reset per test. - Validate hook cleanup
Ensure every
beforeEach,afterEach,before,afterthat returns a Promise eitherawaits it or returns it. Missing returns can leave the test environment in a corrupted state.
Fixes tied to findings
Hanging test
- Return a settled Promise or call
done(). - If the test legitimately needs to wait for external events, increase the timeout with
this.timeout()(in a regular function) or configure a higher global timeout. - Close any resources (servers, sockets, timers) in an
afterorafterEachhook. - Use
--exitonly as a temporary measure; prefer fixing the leak.
Uncaught promise rejection
- Make the test return the Promise:
return doAsyncThing().then(...); - Or use
async/awaitandawaitthe call. - If the rejection is expected, catch it and assert with
assert.rejects.
Timeout misconfiguration
- Replace arrow functions with regular functions when you need Mocha’s test context.
- Set timeout via configuration file (
.mocharc.json):{ "timeout": 5000 } - Verify the actual async work duration with a simple console timer before adjusting the timeout.
Parallel mode race conditions
- Move shared state into test‑local fixtures (e.g., create a fresh DB schema per test).
- If a true singleton is required, serialize access using a mutex or queue.
- Consider disabling parallel mode (
--parallel) for suites that cannot be isolated.
Hook cleanup failures
- Return or await cleanup Promises.
- Add
--bailto stop the suite on the first hook failure, making debugging easier. - Log any hook errors to confirm they are not silently ignored.
Escalation criteria
Escalate to a senior engineer or open an issue in the project’s repository when:
- The hanging persists after verifying that all tests return/Promise‑settle and all handles are closed.
- Uncaught rejection warnings appear in multiple test files despite proper returns, suggesting a global Promise monkey‑patch.
- Timeout errors continue after raising the timeout well beyond observed async durations, indicating a possible Mocha bug or environment issue (e.g., CPU throttling in CI).
- Flaky failures remain after isolating state and running with
--jobs=1, pointing to a deeper framework or library incompatibility.
In each case, capture:
- Mocha and Node versions.
- The exact command used (including flags).
- Minimal reproduction steps.
- Console output (including warnings).
Practical verification
After applying a fix, run the following to confirm the problem is resolved:
npx mocha test/example.js --exit– should exit promptly with a clear pass/fail status.npx mocha test/example.js– no uncaught rejection warnings should appear.npx mocha test/ --parallel --jobs=4– the suite should finish with a deterministic outcome (no flakiness across multiple runs).
If the exit code is 0 for a passing suite or non‑zero for a failing suite, and no warnings are printed, the diagnostic steps have succeeded.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.