Direct answer
Nimble does not install a global unhandledRejection listener that converts every stray rejection into a test failure. In current 2.x and 3.x releases, a rejected promise fails the test only when that promise is returned from the test function or awaited inside an async test. If the test body creates a rejected promise but lets it float—neither returned nor awaited—Nimble will not see it, and the test can pass silently.
What is confirmed vs. what depends on your setup
The documented mechanism is the promise-returning test pattern: Nimble wraps the value returned by your test function, and if that value is a promise which rejects, the wrapper converts the rejection into an AssertionError that the runner reports. This is the intended, supported path.
What is not guaranteed is detection of rejections that never reach the test's return value. A promise created inside the test but not returned, or a rejection scheduled after the test has already completed, remains an ordinary unhandled rejection in the Node.js event loop. Nimble's default configuration does not alter global unhandledRejection handling, so such failures may only appear as process-level warnings rather than test failures.
Version assumption
The automatic promise-return detection described here is consistent across Nimble 2.x and 3.x. Older forks or pre-2.0 versions may require an explicit .catch() or await even when a promise is returned. Because this behavior is tied to the runner's promise wrapper rather than a universal Node.js hook, verify the exact behavior against the Nimble version pinned in your project.
Fix for this case
Ensure every asynchronous operation under test is part of the test's returned promise chain:
- Declare the test
async and await the promise directly. - Or, return the promise from the test body without
await. - If you use the legacy
done callback style, call done(error) when a promise rejects; Nimble will not intercept it automatically.
// Fails correctly: rejection is awaited
it('rejects when input is invalid', async () => {
await expect(validate(null)).to.be.rejected();
});
// Fails correctly: rejection is returned
it('rejects when input is invalid', () => {
return expect(validate(null)).to.be.rejected();
});
// Silent pass risk: floating promise
it('rejects when input is invalid', () => {
expect(validate(null)).to.be.rejected(); // not returned or awaited
});
Verification
To confirm the behavior in your environment, create a minimal test that returns a rejecting promise and run your suite. Then create a second test that invokes the same rejecting promise without returning or awaiting it. The first should fail with an AssertionError; the second should pass unless you add an explicit await, return, or .catch() that rethrows. If the second test still fails, a wrapper or global handler in your project may already be capturing unhandled rejections.
One caveat
If a library inside your test swallows rejections internally—for example, catching and logging them—Nimble cannot observe the failure unless you rethrow the error or assert on the swallowed result.