{"record":{"id":"c835568b4aafc6bc","repo":"actualbudget/actual","slug":"tag-not-found","errorCode":null,"errorMessage":"Tag not found","messagePattern":"Tag not found","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/loot-core/src/server/tags/app.ts","lineNumber":129,"sourceCode":"  });\n  return tag;\n}\n\nasync function renameTag({\n  id,\n  tag: newTag,\n}: Pick<TagEntity, 'id' | 'tag'>): Promise<TagEntity['id']> {\n  const name = newTag.trim();\n  // accept any char except whitespaces and '#', same as tag creation\n  if (!/^[^#\\s]+$/.test(name)) {\n    throw new Error('Invalid tag name');\n  }\n\n  const tags = await db.getTags();\n  const allTags = await db.getAllTags();\n  const existing = tags.find(t => t.id === id);\n  if (!existing) {\n    throw new Error('Tag not found');\n  }\n  if (existing.tag === name) {\n    return id;\n  }\n  if (allTags.some(t => t.id !== id && t.tag === name)) {\n    throw new Error('A tag with that name already exists');\n  }\n\n  await batchMessages(async () => {\n    await db.updateTag({ id, tag: name });\n\n    for (const { id: transactionId, notes } of await db.findTags()) {\n      const renamed = renameTagInNotes(notes, existing.tag, name);\n      if (renamed !== notes) {\n        await db.updateTransaction({ id: transactionId, notes: renamed });\n      }\n    }\n  });","sourceCodeStart":111,"sourceCodeEnd":147,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/loot-core/src/server/tags/app.ts#L111-L147","documentation":"renameTag() looks up the tag by id via db.getTags() and throws Error('Tag not found') when no existing tag has that id. This is an existence check before applying a rename, preventing updates to deleted or never-existing tags.","triggerScenarios":"renameTag({ id, tag }) is called with an id that no longer exists (tag deleted elsewhere/concurrently), a stale id from an outdated UI list, or a malformed/unknown id.","commonSituations":"Two tabs/devices where one deletes the tag while the other renames it; UI retaining stale state after deletion; API/plugin code passing a wrong id.","solutions":["Re-fetch the tag list before renaming and confirm the id still exists","Handle deletion of the tag gracefully in the UI instead of attempting a rename","If ids come from another device, re-sync first so the local tag set is current"],"exampleFix":"// before\nawait renameTag({ id: staleId, tag: 'new' }); // Error: Tag not found\n// after\nconst tags = await getTags();\nif (tags.some(t => t.id === id)) await renameTag({ id, tag: 'new' });","handlingStrategy":"validation","validationCode":"const tags = await getTags();\nif (!tags.some(t => t.id === id)) {\n  throw new Error(`Tag ${id} does not exist; refresh and retry`);\n}","typeGuard":"function isTagNotFound(e: unknown): boolean {\n  return e instanceof Error && e.message === 'Tag not found';\n}","tryCatchPattern":"try {\n  await renameTag({ id, tag });\n} catch (e) {\n  if (isTagNotFound(e)) {\n    await refreshTagList(); // drop stale id from UI\n  } else throw e;\n}","preventionTips":["Re-fetch tags after any delete before renaming","Refresh local state on cross-device changes","Avoid caching tag ids across long-lived sessions"],"tags":["tags","not-found"],"backgroundTag":"resource-not-found","analyzedSha":"d4334cb6e6123f4d3bcea1ad6166608884c7e658","analyzedAt":"2026-08-29T01:02:11.213Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}