Synchronous Throw Assertion On An Async Callback

ID

javascript.sync_assertion_async_callback

Severity

high

Remediation Complexity

trivial

Remediation Risk

low

Remediation Effort

low

Resource

Reliability

Language

JavaScript

Tags

assertion, async, reliability, testing

Description

Reports a synchronous throw assertion — expect(fn).toThrow(), expect(fn).to.throw(), assert.throws(fn) — whose callback is async or returns a promise.

These matchers call the function and catch what it throws on the way out. An async function throws nothing on the way out: it returns a promise and rejects it later, so there is nothing for the matcher to catch.

The asynchronous spellings are the fix and are never reported: .rejects.toThrow(), assert.rejects(), .to.be.rejectedWith(). A resolves or rejects anywhere in the chain is taken as the author having said the assertion is asynchronous.

Only a function written at the call site is examined. An identifier naming a function declared elsewhere is not reported, since whether that one is async is a question about another declaration.

Rationale

// Bad
expect(async () => charge(cart)).toThrow('declined');
assert.throws(async () => charge(cart), Error);
  • The assertion fails with "received function did not throw" even when the code under test rejects exactly as the test says it should.

  • Because the message blames the code and not the assertion, the usual response is to weaken or delete the test.

  • The negated form, expect(asyncFn).not.toThrow(), has the opposite failure mode: it passes unconditionally and protects nothing.

  • The rejected promise nobody is holding surfaces later as an unhandled rejection somewhere else in the run.

Remediation

Use the asynchronous assertion for an asynchronous function.

// Good
await expect(charge(cart)).rejects.toThrow('declined');
await assert.rejects(() => charge(cart), Error);

© 2026 Xygeni. All rights reserved