Code After The done() Handshake

ID

javascript.test_code_after_done

Severity

high

Remediation Complexity

trivial

Remediation Risk

low

Remediation Effort

low

Resource

Reliability

Language

JavaScript

Tags

async, reliability, testing

Description

Reports code that follows the done() handshake in a test or hook callback. Calling done() tells the runner the test is over: it records the outcome, tears the fixture down and moves on. The statements after it still execute, but outside the test the reader believes they belong to.

The call is only read as the handshake when its name is a parameter of the enclosing test or hook callback, so a project’s own done() function is not mistaken for it.

Only the statements beside it in the same block count, which is what makes the if (err) { done(err); return; } guard correct rather than reported. A break or a continue reads the same way.

Rationale

// Bad
it('loads the row', (done) => {
  fetchRow().then((row) => {
    done();
    expect(row.id).toBe(7);      // decided after the test was recorded as passed
  });
});
  • The assertion cannot fail the test that contains it: it throws into an already-settled test.

  • The runner either swallows that throw or attributes it to whatever test is running by then, so a real regression surfaces as a confusing failure somewhere else.

  • The file reads as checking row.id and does not check it, which is exactly the kind of gap a test suite is supposed to rule out.

  • Fixture teardown may already have run, so the values the trailing code inspects are not the ones the test set up.

Remediation

Call done() last, after everything the test has to check.

// Good
it('loads the row', (done) => {
  fetchRow().then((row) => {
    expect(row.id).toBe(7);
    done();
  });
});

© 2026 Xygeni. All rights reserved