actualbudget/actual · error · APIError
Cannot end a batch process: no batch started
Error message
Cannot end a batch process: no batch started
What it means
api.batchBudgetEnd() resolves the promise created by batchBudgetStart() and clears the module-level batchPromise. If no batch was started, there is nothing to resolve, so the handler throws this APIError.
Source
Thrown at packages/loot-core/src/server/api.ts:156
// 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 };
});
});
}
};
handlers['api/batch-budget-end'] = async function () {
if (!batchPromise) {
throw APIError('Cannot end a batch process: no batch started');
}
batchPromise.resolve();
batchPromise = null;
};
handlers['api/load-budget'] = async function ({ id }) {
const { id: currentId } = prefs.getPrefs() || {};
if (currentId !== id) {
connection.send('start-load');
const { error } = await handlers['load-budget']({ id });
if (!error) {
connection.send('finish-load');
} else {
connection.send('show-budgets');
View on GitHub (pinned to d4334cb6e6)
Solutions
- Only call batchBudgetEnd() after batchBudgetStart() succeeded — move the end call inside try/finally that starts after the try begins
- Guard with a flag: only end when a start was recorded
- Check for double-invocation (e.g. both a success path and a finally path calling end)
Example fix
// before
try {
await api.batchBudgetStart();
applyUpdates(items);
} finally {
await api.batchBudgetEnd(); // throws if start threw
}
// after
await api.batchBudgetStart();
try {
applyUpdates(items);
} finally {
await api.batchBudgetEnd();
} Defensive patterns
Strategy: validation
Validate before calling
if (!batchPromise) return;
Try / catch
if (!batchPromise) {
logger.warn('batch end with no batch; ignoring');
return;
}
batchPromise.resolve();
batchPromise = null; Prevention
- Make end idempotent or no-op when no batch is active
- Track batch lifecycle state explicitly
- Log end calls to catch double-invocations
When it happens
Trigger: Calling api.batchBudgetEnd() without a preceding api.batchBudgetStart(), calling it twice, or calling it after a failed start (which never set batchPromise).
Common situations: Cleanup code in a finally block that runs even when batchBudgetStart() threw; duplicated end calls across retries; copy-pasted code paths sharing one end call.
Related errors
- No budget file is open
- Cannot start a batch process: batch already started
- Error importing budget: ${result.error}
- Error importing budget: no budget was loaded
- Error exporting budget: ${result.error}
AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29).
Data as JSON: /api/errors/dac4e5f369a564b4.
Report an issue: GitHub.