avajs/ava · error · TypeError

Test & hook titles must be strings

Error message

Test & hook titles must be strings

What it means

AVA validates that any explicitly provided test or hook title is a string. parseTestArgs sets title.isValid only when the value is a string; if a title was supplied but is not a string (number, object, null, template returning non-string, etc.), the constructor throws this TypeError. Titles are used as identifiers for snapshots, --match, and reporting, so they must be strings.

Source

Thrown at lib/runner.js:135

				}

				// --match selects TODO tests.
				metadata.selected &&= isTitleMatch(title.value, this.matchPatterns);

				this.tasks.todo.push({title: title.value, metadata});
				this.emit('stateChange', {
					type: 'declared-test',
					title: title.value,
					knownFailing: false,
					todo: true,
				});
			} else {
				if (typeof implementation !== 'function') {
					throw new TypeError('Expected an implementation. Use `test.todo()` for tests without an implementation.');
				}

				if (title.isSet && !title.isValid) {
					throw new TypeError('Test & hook titles must be strings');
				}

				let fallbackTitle = title.value;
				if (title.isEmpty) {
					if (metadata.type === 'test') {
						throw new TypeError('Tests must have a title');
					} else if (metadata.always) {
						fallbackTitle = `${metadata.type}.always hook`;
					} else {
						fallbackTitle = `${metadata.type} hook`;
					}
				}

				if (metadata.type === 'test' && !this.registerUniqueTitle(title.value)) {
					throw new Error(`Duplicate test title: ${title.value}`);
				}

				const task = {

View on GitHub (pinned to bbfd946322)

Solutions

  1. Make the title a string: wrap with String(...) or use a template literal
  2. Fix the title variable so it holds a real string at call time
  3. Move a mistakenly placed implementation argument to its correct position in the call
  4. Log typeof title just before the call to find where the non-string comes from

Example fix

// before
const id = 42;
test(id, t => { ... });

// after
test(`test ${id}`, t => { ... });
Defensive patterns

Strategy: type-guard

Validate before calling

if (title !== undefined && typeof title !== 'string') {
	throw new TypeError('Test titles must be strings');
}
test(title, fn);

Type guard

function isValidTitle(title) {
	return title === undefined || typeof title === 'string';
}

Try / catch

try {
	test(title, fn);
} catch (err) {
	if (err.message === 'Test & hook titles must be strings') {
		test(String(title), fn); // coerce and retry declaration
	} else {
		throw err;
	}
}

Prevention

When it happens

Trigger: test(42, fn), test({a: 1}, fn), test(null, fn), or calling an assertion-style API like t => {} with a non-string title variable, e.g. test(myTitle, fn) where myTitle is a number or undefined-but-set value.

Common situations: Passing a numeric constant or enum as the title; a variable title that is undefined at runtime but was 'set' via destructuring; migrating code where the title was accidentally dropped into the implementation slot; template literals are fine, but null/undefined variables are not.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


AI-assisted analysis of avajs/ava@bbfd946322 (2026-09-02). Data as JSON: /api/errors/4488336a226222f2. Report an issue: GitHub.