actualbudget/actual · error
Failed to duplicate budget file: ${error.message}
Error message
Failed to duplicate budget file: ${error.message} What it means
Inside `duplicateBudget`, after validation the budget directory is copied to a new id. If any filesystem operation during the copy throws, the code attempts a best-effort cleanup of the partially created target directory (removing it recursively, ignoring cleanup errors) and then re-throws wrapped as `Failed to duplicate budget file: <original message>`. This indicates an I/O-level failure, not a naming problem — by the time this fires the name was already validated.
Source
Thrown at packages/loot-core/src/server/budgetfiles/app.ts:373
// write metadata for new budget
await fs.writeFile(
fs.join(newBudgetDir, 'metadata.json'),
JSON.stringify(metadata),
);
await fs.copyFile(
fs.join(budgetDir, 'db.sqlite'),
fs.join(newBudgetDir, 'db.sqlite'),
);
} catch (error) {
// Clean up any partially created files
try {
const newBudgetDir = fs.getBudgetDir(newId);
if (await fs.exists(newBudgetDir)) {
await fs.removeDirRecursively(newBudgetDir);
}
} catch {} // Ignore cleanup errors
throw new Error(`Failed to duplicate budget file: ${error.message}`);
}
// load in and validate
const { error } = await _loadBudget(newId);
if (error) {
logger.log('Error duplicating budget: ' + error);
return error;
}
if (cloudSync) {
try {
await cloudStorage.upload();
} catch (error) {
logger.warn('Failed to sync duplicated budget to cloud:', error);
// Ignore any errors uploading. If they are offline they should
// still be able to create files.
}
}View on GitHub (pinned to d4334cb6e6)
Solutions
- Inspect the wrapped `error.message` suffix — it names the underlying OS failure (ENOENT, EACCES, ENOSPC, EBUSY) and points at the actual fix.
- Verify the source budget directory exists and is readable at `fs.getBudgetDir(id)`, and that the parent budgets directory is writable by the server process.
- Free disk space or fix permissions (chown/chmod on the data dir, correct the Docker volume uid) as indicated by the message.
- Manually remove any leftover target budget directory from the failed attempt, then retry `duplicateBudget`.
Example fix
// before (shell, as root-created volume) ls -l /data/budgets # owned by root, server runs as uid 1000 // after chown -R 1000:1000 /data/budgets
Defensive patterns
Strategy: try-catch
Validate before calling
import fs from 'fs/promises'; // before duplicating: const srcDir = getBudgetDir(id); await fs.access(srcDir, fs.constants.R_OK); // source readable await fs.access(path.dirname(srcDir), fs.constants.W_OK); // parent writable const stats = await fs.statfs(path.dirname(srcDir)); // check free space (Node >=18.15)
Try / catch
try {
await duplicateBudget({ id, newName, cloudSync: false, open: 'none' });
} catch (e) {
// e.message = 'Failed to duplicate budget file: <os error>'
logger.error('Budget duplication failed:', e.message);
// clean up leftover partial copy if the target dir exists, then retry once
} Prevention
- Keep the Actual data directory on a local, writable volume with ample free space
- Fix ownership/permissions when running in Docker (match the server process uid)
- Watch for wrapped OS codes: ENOSPC (disk full), EACCES (permissions), ENOENT (missing source)
- After a failed duplication, remove the leftover target budget directory before retrying
When it happens
Trigger: Calling `duplicateBudget` when the source budget directory is unreadable/missing on disk, the disk is full, permissions deny writing to the budgets folder, the target directory exists and cannot be removed, or an OS-level error interrupts the recursive copy.
Common situations: Running the sync server against a read-only or quota-exhausted volume; budgets stored on a network mount that dropped mid-copy; permission changes after moving the data directory (e.g. Docker volume owned by another uid); a stale/corrupt target directory left by a previous failed duplication.
Related errors
- error
- 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/305b5eb48bc4a0d2.
Report an issue: GitHub.