withastro/astro · error · Error
ID must be a non-empty string
Error message
ID must be a non-empty string
What it means
Thrown as a plain Error (no AstroError code) from the scoped store's set() method when the provided id (key) is falsy (undefined, null, '', 0, false). Every content entry must have a non-empty string identifier, so a missing id is treated as a loader bug.
Source
Thrown at packages/astro/src/content/mutable-data-store.ts:342
// If there are pending writes, we need to write again to ensure we flush the latest data.
if (this.#pending.has(fileKey)) {
this.#pending.delete(fileKey);
// Call ourself recursively to write the file again
await this.#writeFileAtomic(filePath, data, depth + 1);
}
}
}
scopedStore(collectionName: string): DataStore {
return {
get: <TData extends Record<string, unknown> = Record<string, unknown>>(key: string) =>
this.get<DataEntry<TData>>(collectionName, key),
entries: () => this.entries(collectionName),
values: () => this.values(collectionName),
keys: () => this.keys(collectionName),
set: ({ id: key, data, body, filePath, deferredRender, digest, rendered, assetImports }) => {
if (!key) {
throw new Error(`ID must be a non-empty string`);
}
const id = String(key);
if (digest) {
const existing = this.get<DataEntry>(collectionName, id);
if (existing && existing.digest === digest) {
return false;
}
}
const foundAssets = new Set<string>(assetImports);
// Check for image imports in the data. These will have been prefixed during schema parsing
forEach(data, (_, val) => {
if (typeof val === 'string' && val.startsWith(IMAGE_IMPORT_PREFIX)) {
const src = val.replace(IMAGE_IMPORT_PREFIX, '');
foundAssets.add(src);
}
});
const entry: DataEntry = {View on GitHub (pinned to d081033d5f)
Solutions
- In your loader, validate the id before calling store.set: if (!id) throw ... or skip the entry.
- Ensure generateId (glob loader) always returns a non-empty string.
- Inspect the source data for rows/objects lacking an id and fix or filter them.
- Default to a derived slug when the field is absent.
Example fix
// before - custom loader
for (const row of rows) {
context.store.set({ id: row.key, data: row });
}
// after
for (const row of rows) {
const id = row.key ?? slugify(row.title);
if (!id) continue;
context.store.set({ id, data: row });
} Defensive patterns
Strategy: validation
Validate before calling
function setEntry(store: DataStore, id: unknown, data: Record<string, unknown>) {
if (typeof id !== 'string' || id.length === 0) throw new TypeError('id must be non-empty string');
store.set({ id, data });
} Type guard
const isNonEmptyString = (v: unknown): v is string => typeof v === 'string' && v.length > 0;
Prevention
- Always validate id before store.set in custom loaders.
- Ensure generateId (glob loader) returns a non-empty string for every input.
- Filter or fail loudly on data rows lacking an id.
When it happens
Trigger: A collection loader calls context.store.set({ id: someValue, data }) where someValue is undefined/empty — e.g. deriving id from a missing field, or passing an object whose id property is absent.
Common situations: Custom loader reading a data file whose rows are missing the id field; slug/id generation returning empty for edge-case filenames; glob loader generateId returning ''; conditional id logic that yields undefined.
Related errors
- File path must be relative to the site root. Got: ${filePath
- ContentLoaderReturnsInvalidId
- ContentLoaderInvalidDataError
- 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/46e9d118596ae9df.
Report an issue: GitHub.