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:
- Regex —
assert.throws(fn, /pattern/) ✅ covered by harness test
- Constructor —
assert.throws(fn, TypeError) ✅ covered by harness test
- Object matcher —
assert.throws(fn, { code: 'X', message: 'Y' }) ❌ not covered
- Validator function —
assert.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.
Background
The CTS
assert.throwsglobal delegates tonode:assert/strictin the Node.js implementor, which supports three forms beyond the basic no-matcher call:assert.throws(fn, /pattern/)✅ covered by harness testassert.throws(fn, TypeError)✅ covered by harness testassert.throws(fn, { code: 'X', message: 'Y' })❌ not coveredassert.throws(fn, (err) => { ...; return true; })❌ not coveredProblem
Forms 3 and 4 are already relied on by ported tests:
tests/js-native-api/test_bigint/test.js(lines 41–50), andtests/js-native-api/test_error/test.js(feat: port test_error to CTS #40)tests/js-native-api/test_error/test.js(feat: port test_error to CTS #40)The harness test at
tests/harness/assert.jsdoesn'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
--importflags) 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.throwsspecifically, 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:
.d.tsdeclaration file for the injected globals (e.g.globals.d.ts) that precisely defines which overloads ofassert.throwsare guaranteed..d.tswould 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-syntaxpattern) that flagsassert.throwscalls 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.jsfiles.Possible resolutions
tests/harness/assert.jsso implementors are forced to support them.globals.d.tsand runtsc --noEmit(orts-checkvia JSDoc@typecomments) over the test files to enforce the contract statically.assert.throwsto covered call forms.Surfaced during review of #40.