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

  1. Read the thrown `message` — it comes from `validateBudgetName` and states the exact rule violated; fix `newName` accordingly.
  2. Ensure `newName` is a non-empty, trimmed string without path separators or OS-reserved characters.
  3. Check whether a budget with that name already exists (list budgets via the sync-server/budget files) and pick a unique name.
  4. 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

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


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