withastro/astro · error · AstroError

ContentLoaderInvalidDataError

ContentLoaderInvalidDataError

Error message

**${context.collection}** entry is missing an ID.
Entry missing ID:
${JSON.stringify({ ...raw, id: undefined }, null, 2)}

What it means

After a function-style loader's array passes schema validation, Astro walks it and requires each entry to have a truthy `id`. An empty-string id (`''`) is a valid string for zod but is falsy, so it triggers ContentLoaderInvalidDataError with the offending entry printed with `id: undefined`.

Solutions

  1. Filter or skip blank-id entries inside your loader before returning: `rows.filter((r) => r.id && r.id.trim())`.
  2. Generate an id from another field (title, hash, index) when the source id is blank: `id: p.id || slugify(p.title)`.
  3. Trim ids at ingest so whitespace-only ids do not slip through: `id: String(p.id).trim()`.

Example fix

// before
loader: async () => {
  const rows = await parseCsv('data.csv'); // row: { id: '', title: 'x' }
  return rows;
},

// after
loader: async () => {
  const rows = await parseCsv('data.csv');
  return rows
    .filter((r) => r.id && r.id.trim().length > 0)
    .map((r) => ({ ...r, id: r.id.trim() }));
},
Defensive patterns

Strategy: validation

Validate before calling

const hasAllIds = (rows: Array<Record<string, any>>) =>
  rows.every((r) => typeof r.id === 'string' && r.id.trim().length > 0);

if (!hasAllIds(rows)) {
  const bad = rows.findIndex((r) => !(typeof r.id === 'string' && r.id.trim()));
  throw new Error(`Row ${bad} has a blank id: ${JSON.stringify(rows[bad])}`);
}

Type guard

const hasNonEmptyId = (e: unknown): e is { id: string } =>
  typeof (e as any)?.id === 'string' && (e as any).id.length > 0;

Try / catch

try {
  await loader({ ...context } as any);
} catch (err) {
  if ((err as any)?.code === 'ContentLoaderInvalidDataError' && /missing an ID/.test(err.message)) {
    // message embeds the offending entry; fix the source row and re-run
  } else throw err;
}

Prevention

When it happens

Trigger: A function-style loader returning an array where some entry has `id: ''` — e.g. a blank CSV column, a slugify step producing an empty slug from a title with only special characters, or API rows with missing ids defaulted to empty string.

Common situations: CSV data files with blank id columns; custom slug generators that strip all characters; upstream data where optional ids come back as empty strings instead of null.

Related errors


AI-assisted analysis of withastro/astro@52e6c34790 (2026-08-18). Data as JSON: /api/errors/9efb321d770fba97. Report an issue: GitHub.

Appendix: source

Thrown at packages/astro/src/content/content-layer.ts:505

		const entry = Array.isArray(unsafeData)
			? unsafeData[firstPathItem as number]
			: unsafeData[firstPathItem as string];

		throw new AstroError({
			...AstroErrorData.ContentLoaderReturnsInvalidId,
			message: AstroErrorData.ContentLoaderReturnsInvalidId.message(context.collection, entry),
		});
	}

	const data = parsedData.data;

	context.store.clear();

	if (Array.isArray(data)) {
		for (const raw of data) {
			if (!raw.id) {
				throw new AstroError({
					...AstroErrorData.ContentLoaderInvalidDataError,
					message: AstroErrorData.ContentLoaderInvalidDataError.message(
						context.collection,
						`Entry missing ID:\n${JSON.stringify({ ...raw, id: undefined }, null, 2)}`,
					),
				});
			}
			const item = await context.parseData({ id: raw.id, data: raw });
			context.store.set({ id: raw.id, data: item });
		}
		return;
	}
	if (typeof data === 'object') {
		for (const [id, raw] of Object.entries(data)) {
			if (raw.id && raw.id !== id) {
				throw new AstroError({
					...AstroErrorData.ContentLoaderInvalidDataError,
					message: AstroErrorData.ContentLoaderInvalidDataError.message(

View on GitHub (pinned to 52e6c34790)