zen-browser/desktop · warning · Error
Mods data file is invalid
Error message
Mods data file is invalid
What it means
Thrown by nsZenMods.getMods() after IOUtils.readJSON(this.modsDataFile) successfully parses <profile>/zen-themes.json but the resulting value is null or not an object (e.g. an array, string, number, or boolean). The provider's contract is that zen-themes.json holds a map of modId -> mod record, so a non-object top level is treated as corruption. The throw is control flow: it jumps into the immediately-following catch block (ZenMods.mjs:437) which writes {} back to the file and surfaces the toast "zen-themes-corrupted" (timeout 8000ms). Because the throw is caught locally, callers of getMods() never see an exception — but note the local `mods` variable retains the invalid value and is returned at line 448, a latent quirk worth knowing about.
Source
Thrown at src/zen/mods/ZenMods.mjs:435
getModFolder(modId) {
return PathUtils.join(this.modsRootPath, modId);
}
async getMods() {
if (!(await IOUtils.exists(this.modsDataFile))) {
await IOUtils.writeJSON(this.modsDataFile, {});
return {};
}
let mods = {};
try {
mods = await IOUtils.readJSON(this.modsDataFile);
if (mods === null || typeof mods !== "object") {
throw new Error("Mods data file is invalid");
}
} catch {
// If we have a corrupted file, reset it
await IOUtils.writeJSON(this.modsDataFile, {});
Services.wm
.getMostRecentWindow("navigator:browser")
.gZenUIManager.showToast("zen-themes-corrupted", {
timeout: 8000,
});
}
return mods;
}
async getModPreferences(mod) {
const modPath = PathUtils.join(
this.modsRootPath,View on GitHub (pinned to 89e31cd31f)
Solutions
- Acknowledge the self-heal: the catch already rewrites zen-themes.json to {} and shows the "zen-themes-corrupted" toast. Dismiss the toast and re-enable mods from the Zen mods preferences; no manual file work is required.
- If you want to recover the previous mod list, close Zen, restore <profile>/zen-themes.json from backup BEFORE next launch (the catch resets it on every getMods() call that sees the bad shape), then reopen Zen.
- If the corruption recurs, identify the writer: check whether a profile-sync, dotfiles, or cloud-drive integration is overwriting zen-themes.json, and exclude the Zen profile's zen-themes.json from sync.
- To start clean without the toast, shut Zen down and delete <profile>/zen-themes.json — the exists() guard at line 423 will recreate it as {} on next getMods() without firing the corruption toast.
Example fix
// before: throw on null/non-object, but `mods` keeps the bad value and is returned
mods = await IOUtils.readJSON(this.modsDataFile);
if (mods === null || typeof mods !== "object") {
throw new Error("Mods data file is invalid");
}
// ... catch resets the file on disk but does NOT reset `mods`
return mods;
// after: reset `mods` too, and use a stricter guard that rejects arrays
mods = await IOUtils.readJSON(this.modsDataFile);
if (
mods === null ||
typeof mods !== "object" ||
Array.isArray(mods)
) {
await IOUtils.writeJSON(this.modsDataFile, {});
mods = {}; // ensure returned value is valid
Services.wm
.getMostRecentWindow("navigator:browser")
?.gZenUIManager?.showToast("zen-themes-corrupted", { timeout: 8000 });
}
return mods; Defensive patterns
Strategy: type-guard
Validate before calling
// Run before relying on the mods map; avoids the throw entirely by validating
// shape and falling back to {} when the file is wrong.
async function safeReadMods(modsDataFile) {
if (!(await IOUtils.exists(modsDataFile))) return {};
let parsed;
try {
parsed = await IOUtils.readJSON(modsDataFile);
} catch {
return {}; // invalid JSON syntax — let caller decide on reset/toast
}
return isModsMap(parsed) ? parsed : {};
}
// Call instead of the raw IOUtils.readJSON + throw pattern. Type guard
// Type guard for the Zen mods data file shape: a plain object keyed by modId.
function isModsMap(value) {
if (value === null || typeof value !== "object") return false;
if (Array.isArray(value)) return false;
// every value should itself be a mod record (object)
for (const v of Object.values(value)) {
if (v === null || typeof v !== "object" || Array.isArray(v)) {
return false;
}
}
return true;
}
// Usage:
// const mods = isModsMap(raw) ? raw : {}; Try / catch
// Recommended pattern mirroring the existing local catch, but with `mods`
// reset so callers never receive the invalid value. Use this when wrapping
// getMods() from outside the class.
async function getModsSafe(zenMods) {
let mods;
try {
mods = await zenMods.getMods();
} catch (e) {
// getMods already self-heals on disk; just don't propagate
return {};
}
return isModsMap(mods) ? mods : {};
} Prevention
- Never edit <profile>/zen-themes.json by hand; use the Zen mods preferences UI which writes a validated object.
- Exclude zen-themes.json from profile-sync, dotfiles, and cloud-drive tools — these are the most common source of a non-object top level.
- On every write path (removeMod, enableMod, disableMod, updateMods), assert the value passes isModsMap() before IOUtils.writeJSON so corruption cannot be persisted in the first place.
- Write zen-themes.json atomically (write to temp, then rename) to avoid the partial-write case that can leave a `null` or truncated body.
- Log the invalid value's type/tag in the catch so the source of corruption is diagnosable instead of silent.
When it happens
Trigger: zen-themes.json exists (the exists() guard at line 423 passed) AND IOUtils.readJSON succeeds (no SyntaxError, so the bytes are valid JSON) AND the parsed value satisfies `mods === null || typeof mods !== 'object'`. Concretely: file contents are exactly `null`; contents are a JSON array like `["foo"]` (typeof === 'object' but the modId-keyed lookup downstream breaks — though note Array passes the typeof check, only null fails it, so an array would NOT throw here despite being wrong); contents are a quoted string, number, or boolean. The throw fires only for the null / non-object primitives.
Common situations: User or a sync/backup tool (e.g. profile-sync, dotfiles manager) replaced zen-themes.json with `null` or a non-object JSON value. A partial write during a crash left the file containing just `null`. A hand-edit to migrate or inspect mods accidentally saved a scalar. Downgrade from a future schema that used a different top-level shape. Note: a syntactically broken file (truncated/garbled) does NOT trigger this error — it triggers IOUtils.readJSON's own exception, which lands in the same catch via a different path.
Related errors
AI-assisted analysis of zen-browser/desktop@89e31cd31f (2026-08-13).
Data as JSON: /api/errors/1bffa58a2f8aaf70.
Report an issue: GitHub.