withastro/astro · error · AstroError
ContentLoaderReturnsInvalidId
ContentLoaderReturnsInvalidId
Error message
The content loader for the collection **${collection}** returned an entry with an invalid `id`:
${entry} What it means
`simpleLoader` (used when a collection loader is a function) validates the returned data against a schema that requires each entry to have a string `id`. If validation fails, Astro extracts the offending entry from the union parse issue and throws `ContentLoaderReturnsInvalidId` showing that entry, so the loader author can see exactly which record is malformed.
Source
Thrown at packages/astro/src/content/content-layer.ts:492
) {
const unsafeData = await handler();
const parsedData = loaderReturnSchema.safeParse(unsafeData);
if (!parsedData.success) {
const issue = parsedData.error.issues[0] as z.core.$ZodIssueInvalidUnion;
// Due to this being a union, zod will always throw an "Expected array, received object" error along with the other errors.
// This error is in the second position if the data is an array, and in the first position if the data is an object.
const parseIssue = Array.isArray(unsafeData) ? issue.errors[0] : issue.errors[1];
const error = parseIssue[0];
const firstPathItem = error.path[0];
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)}`,
),View on GitHub (pinned to d081033d5f)
Solutions
- Map each returned item to an object with a string `id` before returning from the loader.
- If returning an object map, use the key as the id (do not also set a conflicting `id`).
- Log the loader's raw return value once to inspect the shape against the schema.
Example fix
// before
const loader = () => fetch('/api/posts').then(r => r.json());
// after
const loader = () => fetch('/api/posts').then(r => r.json()).then(arr => arr.map(p => ({ id: String(p.slug), ...p }))); Defensive patterns
Strategy: validation
Validate before calling
const out = await loader();
const entries = Array.isArray(out) ? out : Object.values(out);
if (!entries.every(e => e && typeof e.id === 'string')) {
throw new Error('Every entry must have a string id');
} Type guard
function entriesHaveStringId(data): data is Array<{ id: string }> {
return Array.isArray(data) && data.every(e => e && typeof e.id === 'string');
} Prevention
- Normalize source records to a string `id` inside the loader.
- Map foreign id fields (slug, _id, key) to `id` before returning.
- Add a unit test asserting every entry has a string id.
When it happens
Trigger: A function-style collection loader returns an array or object whose entries fail the `{ id: string }` shape — e.g. entries where `id` is a number, missing, null, or an object.
Common situations: Fetching from a CMS whose items use `slug`/`_id` instead of `id`; transforming API responses without normalizing the id field; returning raw DB rows where the PK is named differently.
Related errors
- ContentLoaderInvalidDataError
- ID must be a non-empty string
- File path must be relative to the site root. Got: ${filePath
- Live content collections must be defined in "src/live.config
- Collection loader for ${name} does not have a load method
AI-assisted analysis of withastro/astro@d081033d5f (2026-08-12).
Data as JSON: /api/errors/d01bef4774abf678.
Report an issue: GitHub.