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

  1. Call await api.loadBudget('/path/to/budget.sync') (or api.downloadBudget / createBudget) before any other API call
  2. Check that the budgetPath / data directory passed to api.init() actually contains a budget and the server URL/token are correct
  3. 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

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


AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29). Data as JSON: /api/errors/2cb48974fd10f4ee. Report an issue: GitHub.