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

Labels

No labels
No labels

Type

No type

Projects

Status

Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions