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.