Skip to content

assert.throws: harness test doesn't cover object-matcher and validator-function forms #42

@kraenhansen

Description

@kraenhansen

Background

The CTS assert.throws global delegates to node:assert/strict in the Node.js implementor, which supports three forms beyond the basic no-matcher call:

  1. Regexassert.throws(fn, /pattern/) ✅ covered by harness test
  2. Constructorassert.throws(fn, TypeError) ✅ covered by harness test
  3. Object matcherassert.throws(fn, { code: 'X', message: 'Y' }) ❌ not covered
  4. Validator functionassert.throws(fn, (err) => { ...; return true; }) ❌ not covered

Problem

Forms 3 and 4 are already relied on by ported tests:

The harness test at tests/harness/assert.js doesn't verify these forms, so a future runtime implementor who only covers the regex and constructor forms would silently pass the harness test but then fail on the actual ported tests.

Root cause: globals without type contracts

The broader issue is that the CTS allow-lists globals (via --import flags) but doesn't enforce a type contract on them. The harness test files (tests/harness/*.js) are the only mechanism ensuring implementors provide the right shape — and they only go as far as the author thought to test.

For assert.throws specifically, this means the four call signatures are silently assumed to be supported without anything preventing a test file from using an uncovered form.

If the test files (and ideally the harness implementations) were subject to TypeScript type-checking, this class of problem would be caught statically:

  • The CTS could publish a .d.ts declaration file for the injected globals (e.g. globals.d.ts) that precisely defines which overloads of assert.throws are guaranteed.
  • Any test file using an uncovered overload would be a type error at check time, before it ever reaches a runtime implementor.
  • The harness .d.ts would become the canonical contract — tighter than prose documentation and automatically enforced.

Node.js test files are plain .js (CJS), so this gap doesn't exist upstream — but it's inherent to the CTS's multi-runtime design.

Alternative: ESLint-based enforcement

If full TypeScript checking is too heavy, a lighter option is a custom ESLint rule (or a no-restricted-syntax pattern) that flags assert.throws calls whose second argument is neither a regex literal, a constructor reference, nor an explicit allowlist of known-good forms. This is less precise than types but requires no build step and works on plain .js files.

Possible resolutions

  • Short term: Add coverage for forms 3 and 4 to tests/harness/assert.js so implementors are forced to support them.
  • Medium term: Introduce a globals.d.ts and run tsc --noEmit (or ts-check via JSDoc @type comments) over the test files to enforce the contract statically.
  • Alternative: Add an ESLint rule restricting assert.throws to covered call forms.

Surfaced during review of #40.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Status

    Need Triage

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions