actualbudget/actual · error · APIError
No budget file is open
Error message
No budget file is open
What it means
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.
Source
Thrown at packages/loot-core/src/server/api.ts:125
}
const row = await db.first<Pick<db.DbCategory, 'is_income'>>(
'SELECT is_income FROM categories WHERE id = ?',
[id],
);
if (!row) {
throw APIError(`${debug}: category "${id}" does not exist`);
}
if (row.is_income !== 0) {
throw APIError(`${debug}: category "${id}" is not an expense category`);
}
}
function checkFileOpen() {
if (!(prefs.getPrefs() || {}).id) {
throw APIError('No budget file is open');
}
}
let batchPromise = null;
handlers['api/batch-budget-start'] = async function () {
if (batchPromise) {
throw APIError('Cannot start a batch process: batch already started');
}
// If we are importing, all we need to do is start a raw database
// transaction. Updating spreadsheet cells doesn't go through the
// syncing layer in that case.
if (IMPORT_MODE) {
void db.asyncTransaction(() => {
return new Promise((resolve, reject) => {
batchPromise = { resolve, reject };
});View on GitHub (pinned to d4334cb6e6)
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
Example fix
// before
await api.init({ budgetData: { budgetPath } });
const accts = await api.getAccounts();
// after
await api.init({ budgetData: { budgetPath } });
await api.loadBudget(budgetPath);
const accts = await api.getAccounts(); Defensive patterns
Strategy: try-catch
Validate before calling
if (!(prefs.getPrefs() || {}).id) await api.loadBudget(path); Type guard
function hasOpenBudget(p) {
return Boolean(p && p.id);
} Try / catch
try {
checkFileOpen();
} catch (e) {
if (e instanceof APIError && e.message === 'No budget file is open') {
await loadBudget(defaultPath);
} else throw e;
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Cannot end a batch process: no batch started
- Error importing budget: ${result.error}
- Error importing budget: no budget was loaded
- Error exporting budget: ${result.error}
- Error exporting budget: no data was returned
AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29).
Data as JSON: /api/errors/2cb48974fd10f4ee.
Report an issue: GitHub.