actualbudget/actual · error · APIError
Cannot start a batch process: batch already started
Error message
Cannot start a batch process: batch already started
What it means
api.batchBudgetStart() opens a single mutation/transaction scope for bulk updates. The module keeps a module-level batchPromise; if a batch is already open, a second start would nest transactions, so the API throws instead.
Source
Thrown at packages/loot-core/src/server/api.ts:133
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 };
});
});
} else {
void batchMessages(() => {
return new Promise((resolve, reject) => {
batchPromise = { resolve, reject };
});
});
}View on GitHub (pinned to d4334cb6e6)
Solutions
- Ensure every batchBudgetStart() is paired with batchBudgetEnd() in a try/finally
- Track a boolean in your code so only one batch runs at a time, or serialize callers
- If a stale batch is stuck (e.g. after a crash), restart the API process to reset the module state
Example fix
// before
await api.batchBudgetStart();
applyUpdates(items);
await api.batchBudgetEnd();
// after
await api.batchBudgetStart();
try {
applyUpdates(items);
} finally {
await api.batchBudgetEnd();
} Defensive patterns
Strategy: validation
Validate before calling
if (batchPromise) throw APIError('Cannot start a batch process: batch already started'); Try / catch
if (batchPromise) {
try {
await batchPromise.resolve();
} catch (e) {
logger.error('stale batch', e);
}
} Prevention
- Null out batchPromise in a finally block so failures can't wedge the batch state
- Expose batch state for diagnostics
- Reject nested batch starts early
When it happens
Trigger: Calling api.batchBudgetStart() twice without an intervening api.batchBudgetEnd() — e.g. concurrent code paths both starting a batch, or a previous batch never ended because of an exception between start and end.
Common situations: Import scripts where an earlier iteration threw inside the batch and the end call was skipped; parallel workers sharing one API instance; re-entrant calls from event handlers.
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/1d48013842364a58.
Report an issue: GitHub.