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
- Filter or skip blank-id entries inside your loader before returning: `rows.filter((r) => r.id && r.id.trim())`.
- Generate an id from another field (title, hash, index) when the source id is blank: `id: p.id || slugify(p.title)`.
- 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
- Trim and check ids when ingesting CSV/API data instead of trusting them to be non-empty.
- Generate fallback ids (slugified title, content hash) rather than shipping empty strings.
- Add a data-quality test over your source files in CI so blank ids fail before Astro sees them.
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
- ContentLoaderReturnsInvalidId
- ExpectedImageOptions
- A content collection is defined with legacy features (e.g…
- Collection loader for
- ID must be a non-empty string
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)