actualbudget/actual · error
${message}
Error message
${message} What it means
`duplicateBudget` first validates the requested new budget name via `validateBudgetName` before doing any filesystem work. If validation fails, it re-throws the validator's human-readable `message` verbatim. Typical failures are empty/whitespace names, names containing path-hostile characters, or a name that already collides with an existing budget directory (mapping to an existing budget id via `idFromBudgetName`).
Source
Thrown at packages/loot-core/src/server/budgetfiles/app.ts:329
}
}
return 'ok';
}
async function duplicateBudget({
id,
newName,
cloudSync,
open,
}: {
id: Budget['id'];
newName: Budget['name'];
cloudSync: boolean;
open: 'none' | 'original' | 'copy';
}): Promise<Budget['id']> {
const { valid, message } = await validateBudgetName(newName);
if (!valid) throw new Error(message);
const budgetDir = fs.getBudgetDir(id);
const newId = await idFromBudgetName(newName);
// copy metadata from current budget
// replace id with new budget id and budgetName with new budget name
const metadataText = await fs.readFile(fs.join(budgetDir, 'metadata.json'));
const metadata = JSON.parse(metadataText);
metadata.id = newId;
metadata.budgetName = newName;
[
'cloudFileId',
'groupId',
'lastUploaded',
'encryptKeyId',
'lastSyncedTimestamp',
].forEach(item => {View on GitHub (pinned to d4334cb6e6)
Solutions
- Read the thrown `message` — it comes from `validateBudgetName` and states the exact rule violated; fix `newName` accordingly.
- Ensure `newName` is a non-empty, trimmed string without path separators or OS-reserved characters.
- Check whether a budget with that name already exists (list budgets via the sync-server/budget files) and pick a unique name.
- Wrap the call in a try/catch and surface `error.message` to the user in the rename/duplicate dialog instead of failing silently.
Example fix
// before
await duplicateBudget({ id, newName: ' ', cloudSync: false, open: 'none' });
// after
const newName = 'Budget 2026 Copy';
if (newName.trim().length > 0) {
await duplicateBudget({ id, newName, cloudSync: false, open: 'none' });
} Defensive patterns
Strategy: try-catch
Validate before calling
function validateBudgetNameClient(name) {
if (typeof name !== 'string' || name.trim().length === 0) return 'Budget name is required';
if (/[\\/:*?"<>|]/.test(name)) return 'Budget name contains invalid characters';
return null; // ok
} Type guard
function isValidBudgetName(name) {
return typeof name === 'string' && name.trim().length > 0 && !/[\\/:*?"<>|]/.test(name);
} Try / catch
try {
await duplicateBudget({ id, newName, cloudSync: false, open: 'none' });
} catch (e) {
showValidationError(e.message); // message comes from validateBudgetName
} Prevention
- Always trim and non-empty-check the new name before calling
- Strip or reject path-hostile characters (/, \, :, and OS-reserved ones)
- Check for an existing budget with the same name/id before duplicating
- Surface e.message directly to the user in the duplicate dialog — it is already user-readable
When it happens
Trigger: Calling `duplicateBudget({ id, newName, ... })` through the server/app API where `newName` is empty, contains invalid characters (e.g. `/`, `\`, leading dots), or normalizes to a budget id that already exists on disk.
Common situations: A UI lets the user submit the duplicate dialog with a blank name; an automation script passes an unsanitized filename; renaming conflicts with an existing budget because Actual derives the budget id from the name; OS-specific forbidden characters (colon on Windows, etc.).
Related errors
- Invalid BUDGET_QUERY dimension: ${dimension}
- Invalid property "${property}". Property name contains inval
- Invalid property "${property}". Property name cannot end wit
- Cannot transfer between income and expense categories.
- Amount to hold needs to be greater than 0
AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29).
Data as JSON: /api/errors/97c710b9f082eb85.
Report an issue: GitHub.