{"record":{"id":"722718d9c3545f60","repo":"actualbudget/actual","slug":"no-budget-exists-for-month-month","errorCode":null,"errorMessage":"No budget exists for month: ${month}","messagePattern":"No budget exists for month: (.+?)","errorType":"exception","errorClass":"APIError","httpStatus":null,"severity":"error","filePath":"packages/loot-core/src/server/api.ts","lineNumber":99,"sourceCode":"        return result;\n      },\n      { undoDisabled: true },\n    );\n  };\n}\n\nlet handlers = {} as unknown as Handlers;\n\nasync function validateMonth(month) {\n  if (!month.match(/^\\d{4}-\\d{2}$/)) {\n    throw APIError('Invalid month format, use YYYY-MM: ' + month);\n  }\n\n  if (!IMPORT_MODE) {\n    const { start, end } = await handlers['get-budget-bounds']();\n    const range = monthUtils.range(start, end);\n    if (!range.includes(month)) {\n      throw APIError('No budget exists for month: ' + month);\n    }\n  }\n}\n\nasync function validateExpenseCategory(debug, id) {\n  if (id == null) {\n    throw APIError(`${debug}: category id is required`);\n  }\n\n  const row = await db.first<Pick<db.DbCategory, 'is_income'>>(\n    'SELECT is_income FROM categories WHERE id = ?',\n    [id],\n  );\n\n  if (!row) {\n    throw APIError(`${debug}: category \"${id}\" does not exist`);\n  }\n","sourceCodeStart":81,"sourceCodeEnd":117,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/loot-core/src/server/api.ts#L81-L117","documentation":"validateMonth throws APIError when the month is well-formed (YYYY-MM) but falls outside the budget's existing range. Actual queries get-budget-bounds and builds the month range between the budget start and end; the requested month must lie inside it. Thrown only when IMPORT_MODE is off, i.e. in normal API usage.","triggerScenarios":"Calling getBudgetMonth/setBudgetAmount/etc. with a month before the budget was created or after its last budgeted month (e.g. requesting '2023-01' for a budget that starts '2024-01', or a future month beyond created budget pages).","commonSituations":"Reading budgets of a brand-new budget file without creating any months; requesting next year's months on a budget whose bounds end earlier; testing against a demo budget with a limited date range; IMPORT_MODE assumptions differing between scripts and app usage.","solutions":["Query get-budget-bounds and pick a month inside the returned start/end range","Budget the target month in the app (open it in the budget table) to extend the bounds before using it via API","Call the API after ensuring at least one budget month exists in the file","If running bulk imports, set IMPORT_MODE intentionally to skip bounds checks"],"exampleFix":"// before\nawait q.getBudgetMonth('2030-01'); // APIError: No budget exists for month\n// after\nconst { start, end } = await q.run('get-budget-bounds');\nif ('2030-01' >= start && '2030-01' <= end) {\n  await q.getBudgetMonth('2030-01');\n}","handlingStrategy":"validation","validationCode":"const { start, end } = await q.run('get-budget-bounds');\nif (month < start || month > end) {\n  throw new Error(`month ${month} outside budget bounds ${start}..${end}`);\n}","typeGuard":null,"tryCatchPattern":"try {\n  await q.getBudgetMonth(month);\n} catch (e) {\n  if (String(e.message).startsWith('No budget exists for month')) {\n    // clamp month into budget bounds or budget the month first\n  } else throw e;\n}","preventionTips":["Fetch get-budget-bounds and clamp requested months into range","Budget/visit months in the app before scripting against them","For bulk imports, use IMPORT_MODE deliberately","Avoid hard-coding future months in automation"],"tags":["validation","api","budget-bounds"],"backgroundTag":"budget-month-out-of-range","analyzedSha":"d4334cb6e6123f4d3bcea1ad6166608884c7e658","analyzedAt":"2026-08-29T01:02:11.213Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}