briannesbitt/Carbon · error · InvalidArgumentException

$anchorDay must be greater than 0

Error message

$anchorDay must be greater than 0

What it means

setAnchorDay() (reached via the anchorDay parameter of addUnit/add/sub or OverflowMode::AnchorDay) pins a day-of-month that should survive month arithmetic: after adding months the result lands on min(anchorDay, daysInMonth). Day-of-month is 1-based in PHP, so any anchorDay below 1 (0 or negative) is a programming/input error and throws InvalidArgumentException before any date math runs.

Source

Thrown at src/Carbon/Traits/Units.php:600

            return $stringValue;
        }

        if (str_contains($stringValue, 'E')) {
            return number_format($value, 14, '.', '');
        }

        return $stringValue;
    }

    /**
     * Set current day of the instance to the passed value if it exits in the
     * current month, else set current day to the last day of the month.
     */
    public function setAnchorDay(int $anchorDay): static
    {
        if ($anchorDay < 1) {
            throw new InvalidArgumentException('$anchorDay must be greater than 0');
        }

        return $this->day(min($anchorDay, $this->daysInMonth));
    }

    private static function disallowDecimalPart(mixed $value): void
    {
        if (((float) $value) !== ((float) (int) $value)) {
            throw new InvalidArgumentException(
                'Interval objects cannot be multiplied by a non-integer value.',
            );
        }
    }

    private function getOverflowMode(
        OverflowMode|bool|null $overflow = null,
        ?int $anchorDay = null,
    ): ?OverflowMode {

View on GitHub (pinned to b13f05955d)

Solutions

  1. Pass null (omit anchorDay) when there is no anchor instead of 0
  2. Coerce with max(1, $anchorDay) or reject values < 1 at the input boundary
  3. When the source is zero-based (e.g. JS weekday), add 1 before passing
  4. Catch InvalidArgumentException only as a last-resort guard around truly external input

Example fix

// before
$anchor = (int) ($data['pay_day'] ?? 0);
$date->addUnit('month', 1, anchorDay: $anchor);

// after
$anchor = isset($data['pay_day']) ? max(1, (int) $data['pay_day']) : null;
$date->addUnit('month', 1, anchorDay: $anchor);
Defensive patterns

Strategy: validation

Validate before calling

$anchorDay = $data['pay_day'] ?? null;
$anchorDay = $anchorDay === null ? null : max(1, min(31, (int) $anchorDay));
$date->addUnit('month', 1, anchorDay: $anchorDay);

Type guard

function isValidAnchorDay(mixed $value): bool
{
    return $value === null || (is_int($value) && $value >= 1 && $value <= 31);
}

Try / catch

try {
    $date->addUnit('month', 1, anchorDay: $anchor);
} catch (\InvalidArgumentException $e) {
    // only reachable with unvalidated external input - map to 422
    throw new ValidationException($e->getMessage());
}

Prevention

When it happens

Trigger: ->addUnit('month', 1, anchorDay: 0); ->add(1, 'month', overflow: null, anchorDay: -5); anchorDay computed as $date->day - $x where the subtraction hits 0; (int) cast of unvalidated request input ('day' => '0'); an optional field defaulted through (int) null (= 0) instead of null.

Common situations: Off-by-one bugs when deriving the anchor from a zero-based source (JS Date.getDay, array indices, ISO week day numbers); treating 'no anchor' as 0 instead of omitting the argument; form/API input where day 0 means 'not provided' but is forwarded as an int.

Related errors


AI-assisted analysis of briannesbitt/Carbon@b13f05955d (2026-08-17). Data as JSON: /api/errors/068483ebc4477a8c. Report an issue: GitHub.