🔴 The Error You're Seeing

Confirm this matches your console output. If it does, you're in the right place.

ERROR LOG// CAPTURED — OpenJDK 25 Temurin: LocalDate.of(2026, 2, 30) Exception in thread "main" java.time.DateTimeException: Invalid date 'FEBRUARY 30' at java.base/java.time.LocalDate.create(LocalDate.java:461) at java.base/java.time.LocalDate.of(LocalDate.java:277)

⚡ Quick Fix Works 80% of the time

Clamp day-of-month against the real month length before constructing: Math.min(day, YearMonth.of(y, m).lengthOfMonth()).

static LocalDate safeDate(int y, int m, int day) { YearMonth ym = YearMonth.of(y, m); // validates month itself return ym.atDay(Math.min(day, ym.lengthOfMonth())); } safeDate(2026, 2, 30); // -> 2026-02-28, no exception

🧠 Why this Happens

Tap to expand the deep technical explanation

Factories validate the (year, month, day) triple against calendar rules BEFORE allocating anything: month range, then day against that month's true length including the leap-year rule (2026 % 4 != 0, so February holds 28). Failing triples raise DateTimeException quoting the impossible combination verbatim. This loudness is deliberate: java.util.Calendar defaulted to LENIENT, silently rolling 2026-02-30 into March 2nd — data corruption wearing a smile. java.time flips the contract: impossible dates are programmer errors that must surface immediately, and every factory (of, parse under STRICT resolution, plus calculations that would overshoot) enforces the same wall.

The HITEC City Parking Spot Analogy:

Booking a hotel room for November 31st: the reservation system flatly refuses. The old clerk would have quietly booked December 1st and let accounting discover the lie in January.

🔁 How to Reproduce Confirm this is your error

LocalDate.of(2026, 2, 30). (Lab capture: OpenJDK 25.)

🛠️ Solutions (5 Ways to Fix)

Solution 1✓ Most common cause

Clamp/correct with YearMonth lengths

👉 Use this when/if inputs come from spreadsheets or feeds where end-of-month overflow is expected noise.

YearMonth computes the true month length (28/29/30/31) including leap rules; min()-clamping converts impossible days to month-end deterministically. Choose clamping ONLY where policy says so — never as a blanket wrapper hiding upstream bugs.

YearMonth billing = YearMonth.of(year, month); LocalDate due = billing.atDay( Math.min(requestedDay, billing.lengthOfMonth()));
Solution 2

Validate user input before constructing

👉 Use this when/if the triple originates from separate form fields or API parameters.

Reject impossible combinations at the boundary with a message users can act on ('February 2026 has 28 days'), keeping domain objects permanently valid. Construction then never sees garbage, and 500s become clean 400s.

if (day > YearMonth.of(year, month).lengthOfMonth()) { throw new BadRequestException( "%02d/%d has only %d days".formatted(month, year, YearMonth.of(year, month).lengthOfMonth())); }
Solution 3

Prefer plusDays/minusDays arithmetic over manual day math

👉 Use this when/if code builds dates by hand-rolling month lengths and rollover logic.

Hand-computed (year, month+1, day) pairs are where impossible triples are born. LocalDate arithmetic handles month boundaries and leap years internally — plusDays never constructs an invalid state, so the exception cannot fire.

LocalDate next = invoice.plusDays(termsInDays); // safe rollover // instead of: LocalDate.of(y, m + 1, sameDay) <- can explode
Solution 4

Mind Calendar lenient differences when porting legacy code

👉 Use this when/if migrated tests suddenly fail on dates legacy code accepted.

Old Calendar.setLenient(true) rolled invalid dates forward silently; java.time refuses. New test failures on Feb 30-style fixtures are the migration WORKING — fix the fixture expectations rather than re-enabling leniency via third-party shims.

// legacy: Calendar lenient rolled 2026-02-30 -> 2026-03-02 (silently) // modern: LocalDate.of(2026, 2, 30) -> DateTimeException (loudly, correctly)
Solution 5

Unify validation with STRICT resolver parsing

👉 Use this when/if both parsed strings and constructed triples must obey identical rules.

ResolverStyle.STRICT makes parse() raise the same family of errors for impossible textual dates (Feb 30), giving one consistent validity definition across input paths — no more 'parses fine but of() explodes' asymmetry.

DateTimeFormatter strict = DateTimeFormatter .ofPattern("dd/MM/uuuu", Locale.ROOT) .withResolverStyle(ResolverStyle.STRICT); LocalDate.parse("30/02/2026", strict); // rejected exactly like .of()

📋 Version Notes

Java 8

JSR-310 chooses loud failure over Calendar's silent lenient rollover.

Java 11

Messages identical; no behavioral change.

Java 17

Same — the changed error surface versus legacy code is intentional design.

Java 21

Unchanged.

🛡️ How to Prevent This Next Time

Keep domain constructors total (never pass raw user triples straight into of()), centralize clamp-or-reject policy in one place, replace manual calendar arithmetic with plus/minus methods, and add leap-year boundary cases to every date-handling test suite.