{"record":{"id":"1bffa58a2f8aaf70","repo":"zen-browser/desktop","slug":"mods-data-file-is-invalid","errorCode":null,"errorMessage":"Mods data file is invalid","messagePattern":"Mods data file is invalid","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"warning","filePath":"src/zen/mods/ZenMods.mjs","lineNumber":435,"sourceCode":"\n  getModFolder(modId) {\n    return PathUtils.join(this.modsRootPath, modId);\n  }\n\n  async getMods() {\n    if (!(await IOUtils.exists(this.modsDataFile))) {\n      await IOUtils.writeJSON(this.modsDataFile, {});\n\n      return {};\n    }\n\n    let mods = {};\n\n    try {\n      mods = await IOUtils.readJSON(this.modsDataFile);\n\n      if (mods === null || typeof mods !== \"object\") {\n        throw new Error(\"Mods data file is invalid\");\n      }\n    } catch {\n      // If we have a corrupted file, reset it\n      await IOUtils.writeJSON(this.modsDataFile, {});\n\n      Services.wm\n        .getMostRecentWindow(\"navigator:browser\")\n        .gZenUIManager.showToast(\"zen-themes-corrupted\", {\n          timeout: 8000,\n        });\n    }\n\n    return mods;\n  }\n\n  async getModPreferences(mod) {\n    const modPath = PathUtils.join(\n      this.modsRootPath,","sourceCodeStart":417,"sourceCodeEnd":453,"githubUrl":"https://github.com/zen-browser/desktop/blob/89e31cd31f9c72df8a02166fb28fac61160efbdf/src/zen/mods/ZenMods.mjs#L417-L453","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before: throw on null/non-object, but `mods` keeps the bad value and is returned\nmods = await IOUtils.readJSON(this.modsDataFile);\nif (mods === null || typeof mods !== \"object\") {\n  throw new Error(\"Mods data file is invalid\");\n}\n// ... catch resets the file on disk but does NOT reset `mods`\nreturn mods;\n\n// after: reset `mods` too, and use a stricter guard that rejects arrays\nmods = await IOUtils.readJSON(this.modsDataFile);\nif (\n  mods === null ||\n  typeof mods !== \"object\" ||\n  Array.isArray(mods)\n) {\n  await IOUtils.writeJSON(this.modsDataFile, {});\n  mods = {};                       // ensure returned value is valid\n  Services.wm\n    .getMostRecentWindow(\"navigator:browser\")\n    ?.gZenUIManager?.showToast(\"zen-themes-corrupted\", { timeout: 8000 });\n}\nreturn mods;","handlingStrategy":"type-guard","validationCode":"// Run before relying on the mods map; avoids the throw entirely by validating\n// shape and falling back to {} when the file is wrong.\nasync function safeReadMods(modsDataFile) {\n  if (!(await IOUtils.exists(modsDataFile))) return {};\n  let parsed;\n  try {\n    parsed = await IOUtils.readJSON(modsDataFile);\n  } catch {\n    return {};   // invalid JSON syntax — let caller decide on reset/toast\n  }\n  return isModsMap(parsed) ? parsed : {};\n}\n\n// Call instead of the raw IOUtils.readJSON + throw pattern.","typeGuard":"// Type guard for the Zen mods data file shape: a plain object keyed by modId.\nfunction isModsMap(value) {\n  if (value === null || typeof value !== \"object\") return false;\n  if (Array.isArray(value)) return false;\n  // every value should itself be a mod record (object)\n  for (const v of Object.values(value)) {\n    if (v === null || typeof v !== \"object\" || Array.isArray(v)) {\n      return false;\n    }\n  }\n  return true;\n}\n\n// Usage:\n// const mods = isModsMap(raw) ? raw : {};","tryCatchPattern":"// Recommended pattern mirroring the existing local catch, but with `mods`\n// reset so callers never receive the invalid value. Use this when wrapping\n// getMods() from outside the class.\nasync function getModsSafe(zenMods) {\n  let mods;\n  try {\n    mods = await zenMods.getMods();\n  } catch (e) {\n    // getMods already self-heals on disk; just don't propagate\n    return {};\n  }\n  return isModsMap(mods) ? mods : {};\n}","preventionTips":["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."],"tags":["mods","json","file-io","corruption","profile","control-flow"],"backgroundTag":null,"analyzedSha":"89e31cd31f9c72df8a02166fb28fac61160efbdf","analyzedAt":"2026-08-13T03:18:45.463Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}