actualbudget/actual · error

Start date must be before or equal to end date.

Error message

Start date must be before or equal to end date.

What it means

summarySpreadsheet enforces a chronological range: after validating both dates it checks d.isAfter(startDay, endDay) and throws 'Start date must be before or equal to end date.' when the range is inverted. This prevents nonsensical queries (negative or zero-length ranges where the query's $gte bound exceeds $lte) and division issues in per-month/per-year calculations.

Source

Thrown at packages/desktop-client/src/components/reports/spreadsheets/summary-spreadsheet.ts:70

      endDay = d.parse(
        monthUtils.getMonth(end) ===
          monthUtils.getMonth(monthUtils.currentDay())
          ? monthUtils.currentDay()
          : monthUtils.lastDayOfMonth(end),
        'yyyy-MM-dd',
        new Date(),
      );
    } catch (error) {
      console.error('Error parsing dates:', error);
      throw new Error('Invalid date format provided');
    }

    if (!d.isValid(startDay) || !d.isValid(endDay)) {
      throw new Error('Invalid date values provided');
    }

    if (d.isAfter(startDay, endDay)) {
      throw new Error('Start date must be before or equal to end date.');
    }

    const getOneDatePerMonth = (start: Date, end: Date) => {
      const months = [];
      let currentDate = d.startOfMonth(start);

      while (!d.isSameMonth(currentDate, end)) {
        months.push(currentDate);
        currentDate = d.addMonths(currentDate, 1);
      }
      months.push(end);

      return months;
    };

    const makeRootQuery = () =>
      q('transactions')
        .filter({

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Swap the arguments so start <= end at the call site
  2. Validate the ordering before calling: if (new Date(start) > new Date(end)) fix or reject
  3. Clamp/normalize the range in UI code (e.g. date picker constraints) so an inverted range can't be produced
  4. Catch the error and show a user-facing message asking to correct the range

Example fix

// before
summarySpreadsheet('2024-06', '2024-01', ...); // inverted
// after
const [start, end] = ['2024-06', '2024-01'].sort(); // or fix the picker constraint
summarySpreadsheet(start, end, ...);
Defensive patterns

Strategy: validation

Validate before calling

if (new Date(start + '-01') > new Date(end + '-01')) {
  [start, end] = [end, start]; // or reject
}

Type guard

function isOrderedRange(start: string, end: string): boolean {
  return start <= end; // yyyy-MM compares lexicographically
}

Try / catch

try {
  await summarySpreadsheet(start, end, conditions, op, content, locale)(spreadsheet, setData);
} catch (e) {
  if (e instanceof Error && e.message.startsWith('Start date must be')) {
    showUserMessage('Please fix the report date range: start is after end.');
  } else throw e;
}

Prevention

When it happens

Trigger: Calling summarySpreadsheet with start later than end, e.g. start='2024-06', end='2024-01'; also equal-month ranges where end resolves to a day before start's first day (edge cases with currentDay substitution).

Common situations: Swapped start/end arguments at a call site; a date-range picker letting users set an 'end' before 'start'; programmatic range math (subtracting months from start) that overshoots below end.

Related errors


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