evanw/esbuild · error · Error
${quote(property)} must be an array of strings
Error message
${quote(property)} must be an array of strings What it means
Plugin callbacks can declare watchFiles and watchDirs (from onResolve/onLoad results) so esbuild knows which extra paths to monitor in watch mode. sanitizeStringArray (lib/shared/common.ts:1809) iterates the array and throws at :1812 if any element is not a string — esbuild needs string paths to pass to the native fs watcher. The 'property' name in the message is the offending field name (watchFiles or watchDirs).
Source
Thrown at lib/shared/common.ts:1812
messagesClone.push({
id: id || '',
pluginName: pluginName || fallbackPluginName,
text: text || '',
location: sanitizeLocation(location, where, terminalWidth),
notes: notesClone,
detail: stash ? stash.store(detail) : -1,
})
index++
}
return messagesClone
}
function sanitizeStringArray(values: any[], property: string): string[] {
const result: string[] = []
for (const value of values) {
if (typeof value !== 'string') throw new Error(`${quote(property)} must be an array of strings`)
result.push(value)
}
return result
}
function sanitizeStringMap(map: Record<string, any>, property: string): Record<string, string> {
const result: Record<string, string> = Object.create(null)
for (const key in map) {
const value = map[key]
if (typeof value !== 'string') throw new Error(`key ${quote(key)} in object ${quote(property)} must be a string`)
result[key] = value
}
return result
}
function convertOutputFiles({ path, contents, hash }: protocol.BuildOutputFile): types.OutputFile {
// The text is lazily-generated for performance reasons. If no one asks for
// it, then it never needs to be generated.View on GitHub (pinned to 6ff1d8b0d8)
Solutions
- Coerce/filter the array before returning: watchFiles: files.filter(f => typeof f === 'string').
- Use String(p) only if p is path-like; prefer resolving to absolute strings explicitly via path.resolve.
- Add a TS annotation watchFiles?: string[] to surface type errors.
Example fix
// before
build.onLoad({ filter: /.*/ }, args => ({
contents: read(...),
watchFiles: [args.path, getDepCount(args.path)], // second item is a number
}));
// after
build.onLoad({ filter: /.*/ }, args => ({
contents: read(...),
watchFiles: [args.path, ...getDeps(args.path)].filter(
(f): f is string => typeof f === 'string',
),
})); Defensive patterns
Strategy: validation
Validate before calling
function asStringArray(v, name) {
if (!Array.isArray(v)) return undefined;
const out = v.filter((x): x is string => typeof x === 'string');
if (out.length !== v.length) {
throw new TypeError(`${name} must be an array of strings`);
}
return out;
} Type guard
function isStringArray(v): v is string[] {
return Array.isArray(v) && v.every(x => typeof x === 'string');
} Prevention
- Filter watchFiles/watchDirs to strings before returning them from a callback.
- Resolve Path objects to absolute strings via path.resolve before pushing.
- Type these fields as string[] in plugin result types.
When it happens
Trigger: Returning watchFiles: [somePath, null] or watchDirs: [123]. watchFiles containing a Path object (from node:path) instead of a string. Mixing in undefined from an array with holes.
Common situations: Plugin collects candidate files from a glob and forgets to filter out undefined/null. watchFiles from a recursive scan that includes a Buffer or fs.Dirent. Author pushes a Path object from node:path.win32.
Related errors
- Cannot use the "watch" API in this environment
- Plugin at index ${i} must be an object
- Plugin at index ${i} is missing a name
- Plugin is missing a setup function
- Must specify "kind" when calling "resolve"
AI-assisted analysis of evanw/esbuild@6ff1d8b0d8 (2026-08-03).
Data as JSON: /data/errors/f0d60f2a0f1912a4.json.
Report an issue: GitHub.