{"record":{"id":"51a8fa4fbbb1e848","repo":"actualbudget/actual","slug":"unable-to-duplicate-a-budget-that-is-not-local","errorCode":null,"errorMessage":"Unable to duplicate a budget that is not local.","messagePattern":"Unable to duplicate a budget that is not local\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/desktop-client/src/budgetfiles/budgetfilesSlice.ts","lineNumber":203,"sourceCode":"   */\n  cloudSync: boolean;\n};\n\nexport const duplicateBudget = createAppAsyncThunk(\n  `${sliceName}/duplicateBudget`,\n  async (\n    {\n      id,\n      oldName,\n      newName,\n      managePage,\n      loadBudget = 'none',\n      cloudSync,\n    }: DuplicateBudgetPayload,\n    { dispatch },\n  ) => {\n    if (!id) {\n      throw new Error('Unable to duplicate a budget that is not local.');\n    }\n\n    try {\n      dispatch(\n        setAppState({\n          loadingText: t('Duplicating: {{oldName}} to: {{newName}}', {\n            oldName,\n            newName,\n          }),\n        }),\n      );\n\n      await send('duplicate-budget', {\n        id,\n        newName,\n        cloudSync,\n        open: loadBudget,\n      });","sourceCodeStart":185,"sourceCodeEnd":221,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/desktop-client/src/budgetfiles/budgetfilesSlice.ts#L185-L221","documentation":"The `duplicateBudget` thunk in the desktop-client budgetfiles slice throws this when called without a local budget `id`. Duplication works by asking the backend to copy an existing local budget file, so a truthy `id` is a precondition. `DuplicateBudgetPayload.id` is optional (`id?: string`), so a caller that only supplies a `cloudId` (or forgets the id) reaches this guard. The guard exists because cloud-only budgets have no local file to duplicate.","triggerScenarios":"Calling `dispatch(duplicateBudget({...}))` where the payload's `id` field is undefined, null, or an empty string — e.g. dispatching from the budget manager for a file row that only has a `cloudId` (remote-only budget not yet downloaded), or a caller constructing the payload without `id`.","commonSituations":"User clicks 'Duplicate' on a budget in the manager page that exists only in the cloud (never downloaded locally); a UI regression drops the id from the payload; a custom integration/script reuses the thunk with only cloud metadata.","solutions":["Ensure the payload includes the local budget's `id` (the local file id from `loadAllFiles`/budget metadata), not just `cloudId`.","Only offer the Duplicate action for budgets that are downloaded locally; hide or disable it for cloud-only entries.","If the budget is remote-only, first download it (downloadBudget with its cloudFileId) and then duplicate using the returned local id.","Check for stale file metadata — refresh the file list so the local `id` is populated before dispatching."],"exampleFix":"// before\nawait dispatch(duplicateBudget({ cloudId: file.cloudFileId, oldName: file.name, newName, cloudSync: true }));\n// after\nif (!file.id) {\n  alert('Download the budget before duplicating it.');\n  return;\n}\nawait dispatch(duplicateBudget({ id: file.id, oldName: file.name, newName, cloudSync: true }));","handlingStrategy":"validation","validationCode":"const canDuplicate = (file?: { id?: string; cloudFileId?: string }) =>\n  typeof file?.id === 'string' && file.id.length > 0;\nif (!canDuplicate(file)) {\n  alert('This budget is not downloaded locally and cannot be duplicated.');\n  return;\n}\nawait dispatch(duplicateBudget({ id: file.id, ... }));","typeGuard":"function hasLocalId(\n  file: { id?: string | undefined },\n): file is { id: string } {\n  return typeof file.id === 'string' && file.id.length > 0;\n}","tryCatchPattern":"try {\n  await dispatch(duplicateBudget({ id, oldName, newName, cloudSync })).unwrap();\n} catch (e) {\n  if (e instanceof Error && e.message.includes('not local')) {\n    alert('Download this budget first before duplicating it.');\n  } else {\n    throw e;\n  }\n}","preventionTips":["Only render the Duplicate action for budgets with a local id (hasLocalId guard).","Treat cloudId-only files as remote: route them through downloadBudget first.","Refresh the file list (loadAllFiles) before building action payloads so ids are current.","When writing integrations, make `id` required in your own wrapper type instead of relying on the optional payload type."],"tags":["budget","local-first","missing-argument","redux-thunk"],"backgroundTag":"missing-required-argument","analyzedSha":"d4334cb6e6123f4d3bcea1ad6166608884c7e658","analyzedAt":"2026-08-29T01:02:11.213Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}