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

  1. Coerce/filter the array before returning: watchFiles: files.filter(f => typeof f === 'string').
  2. Use String(p) only if p is path-like; prefer resolving to absolute strings explicitly via path.resolve.
  3. 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

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


AI-assisted analysis of evanw/esbuild@6ff1d8b0d8 (2026-08-03). Data as JSON: /data/errors/f0d60f2a0f1912a4.json. Report an issue: GitHub.