{"record":{"id":"2cb48974fd10f4ee","repo":"actualbudget/actual","slug":"no-budget-file-is-open","errorCode":null,"errorMessage":"No budget file is open","messagePattern":"No budget file is open","errorType":"exception","errorClass":"APIError","httpStatus":null,"severity":"error","filePath":"packages/loot-core/src/server/api.ts","lineNumber":125,"sourceCode":"  }\n\n  const row = await db.first<Pick<db.DbCategory, 'is_income'>>(\n    'SELECT is_income FROM categories WHERE id = ?',\n    [id],\n  );\n\n  if (!row) {\n    throw APIError(`${debug}: category \"${id}\" does not exist`);\n  }\n\n  if (row.is_income !== 0) {\n    throw APIError(`${debug}: category \"${id}\" is not an expense category`);\n  }\n}\n\nfunction checkFileOpen() {\n  if (!(prefs.getPrefs() || {}).id) {\n    throw APIError('No budget file is open');\n  }\n}\n\nlet batchPromise = null;\n\nhandlers['api/batch-budget-start'] = async function () {\n  if (batchPromise) {\n    throw APIError('Cannot start a batch process: batch already started');\n  }\n\n  // If we are importing, all we need to do is start a raw database\n  // transaction. Updating spreadsheet cells doesn't go through the\n  // syncing layer in that case.\n  if (IMPORT_MODE) {\n    void db.asyncTransaction(() => {\n      return new Promise((resolve, reject) => {\n        batchPromise = { resolve, reject };\n      });","sourceCodeStart":107,"sourceCodeEnd":143,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/loot-core/src/server/api.ts#L107-L143","documentation":"Actual's public API (api.ts) only allows budget-mutating calls when a budget file is loaded into the running core. checkFileOpen() inspects the preferences store for a budget id and throws this APIError when none is set, guarding every API handler from operating on a non-existent budget.","triggerScenarios":"Calling any api.* method (e.g. api.getAccounts(), api.addTransaction()) after api.init() but before a budget is loaded — i.e. without calling api.loadBudget() or when loadBudget failed silently.","commonSituations":"Scripts/integrations that import @actual-app/api, init it, and immediately query data; CI jobs where the budget download or creation step failed; using the API inside the sync-server without first loading a budget.","solutions":["Call await api.loadBudget('/path/to/budget.sync') (or api.downloadBudget / createBudget) before any other API call","Check that the budgetPath / data directory passed to api.init() actually contains a budget and the server URL/token are correct","Ensure the async init/load chain is awaited; an unawaited loadBudget lets subsequent calls run before prefs.id is set"],"exampleFix":"// before\nawait api.init({ budgetData: { budgetPath } });\nconst accts = await api.getAccounts();\n// after\nawait api.init({ budgetData: { budgetPath } });\nawait api.loadBudget(budgetPath);\nconst accts = await api.getAccounts();","handlingStrategy":"try-catch","validationCode":"if (!(prefs.getPrefs() || {}).id) await api.loadBudget(path);","typeGuard":"function hasOpenBudget(p) {\n  return Boolean(p && p.id);\n}","tryCatchPattern":"try {\n  checkFileOpen();\n} catch (e) {\n  if (e instanceof APIError && e.message === 'No budget file is open') {\n    await loadBudget(defaultPath);\n  } else throw e;\n}","preventionTips":["Initialize prefs with a budget id before exposing the API","Guard all handlers with checkFileOpen() (as api.ts does)","Fail fast at startup when no budget is configured"],"tags":["api","budget-not-loaded","state"],"backgroundTag":"budget-not-loaded","analyzedSha":"d4334cb6e6123f4d3bcea1ad6166608884c7e658","analyzedAt":"2026-08-29T01:02:11.213Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}