Functions That Fail
A function that can be handed bad input has to say so. The usual way is to throw: on success the function returns its result, on failure it throws an Error that explains what went wrong.
A test for such a function must check both paths: good input returns the right value and does not throw, and bad input really does throw — a function that silently accepts bad input is a bug too.
assert.throws(fn) is the check for the bad path. For a function that returns a promise, the same check is spelled assert.rejects(fn) and is awaited.
The Good Path
For a valid input there is nothing extra to write. Call the function, then compare the value. If the call throws instead, the throw ends the test by itself and the report shows the error — you do not have to check for it first.
Valid input, no error
safeDivide(10, 2) must return 5 and must not throw. The call comes first; an unexpected error would end the test right there. Only then is the value compared.
Output: TypeScript file and captured output
import { test } from "node:test"
import assert from "node:assert/strict"
function safeDivide(a: number, b: number): number {
if (b === 0) {
throw new RangeError("cannot divide by zero")
}
return a / b
}
test("safeDivide(10, 2) is 5", () => {
// An unexpected throw ends the test right here.
const got = safeDivide(10, 2)
assert.equal(got, 5, `safeDivide(10, 2) = ${got}; want 5`)
})
The Bad Path
The other half of the contract: bad input must fail. assert.throws takes a small function that makes the call, runs it, and fails if nothing was thrown.
You can also say what should be thrown. Passing the error class as the second argument makes the test fail when some other kind of error comes out.
Bad input must throw
Dividing by zero has to fail. assert.throws calls safeDivide(10, 0) and requires a RangeError. If the function returned a number instead, the test would fail — it accepted input it should have rejected.
Output: TypeScript file and captured output
import { test } from "node:test"
import assert from "node:assert/strict"
function safeDivide(a: number, b: number): number {
if (b === 0) {
throw new RangeError("cannot divide by zero")
}
return a / b
}
test("safeDivide(10, 0) throws", () => {
assert.throws(() => safeDivide(10, 0), RangeError, "expected an error for division by zero")
})
A Table with wantErr
Both paths fit into one table-driven test: give each case a wantErr field saying whether an error is expected. The want field is optional, because a case that throws has no value to compare.
Each subtest looks at wantErr first. Expected failures are checked with assert.throws and stop there; the other cases compare the value as usual.
Good and bad cases in one table
Four cases: three normal divisions and one division by zero with wantErr: true. One loop covers the whole contract of the function, and all four subtests pass.
Output: TypeScript file and captured output
import { test } from "node:test"
import assert from "node:assert/strict"
function safeDivide(a: number, b: number): number {
if (b === 0) {
throw new RangeError("cannot divide by zero")
}
return a / b
}
type Case = {
name: string
a: number
b: number
want?: number
wantErr: boolean
}
const cases: Case[] = [
{ name: "simple division", a: 10, b: 2, want: 5, wantErr: false },
{ name: "negative result", a: -9, b: 3, want: -3, wantErr: false },
{ name: "divide zero", a: 0, b: 5, want: 0, wantErr: false },
{ name: "divide by zero", a: 10, b: 0, wantErr: true }
]
test("safeDivide", async (t) => {
for (const c of cases) {
await t.test(c.name, () => {
if (c.wantErr) {
assert.throws(() => safeDivide(c.a, c.b), RangeError)
return
}
const got = safeDivide(c.a, c.b)
assert.equal(got, c.want, `safeDivide(${c.a}, ${c.b}) = ${got}; want ${c.want}`)
})
}
})