🔴 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.parse("2026-13-01") — impossible month value form Exception in thread "main" java.time.format.DateTimeParseException: Text '2026-13-01' could not be parsed: Invalid value for MonthOfYear (valid values 1 - 12): 13 at java.base/java.time.format.DateTimeFormatter.createError(DateTimeFormatter.java:2079) at java.base/java.time.format.DateTimeFormatter.parse(DateTimeFormatter.java:2014) at java.base/java.time.LocalDate.parse(LocalDate.java:449) // Sibling form — STRUCTURE mismatch reports the character position instead: // java.time.format.DateTimeParseException: Text '2026/08/22' could not be parsed at index 4

⚡ Quick Fix Works 80% of the time

Read the tail of the message: an Invalid value clause means the data is wrong; an at index clause means the pattern or separators are wrong.

try { return LocalDate.parse(input); } catch (DateTimeParseException e) { throw new BadRequestException( "Date must be ISO yyyy-MM-dd, got: " + input); }

🧠 Why this Happens

Tap to expand the deep technical explanation

Parsing runs a formatter state machine across the text building field-value pairs, then resolves those fields into a concrete date. Two independent layers can reject input: the PARSER aborts when text structure diverges from the pattern and reports the failing character offset (at index 4), while the FIELD VALIDATOR accepts well-formed digits but rejects impossible ones like month 13, quoting the rule (valid values 1 - 12). Resolution style then decides borderline cases — SMART rolls February 30 into March, LENIENT overflows aggressively, STRICT rejects. Knowing which layer complained turns guesswork into a targeted fix.

The HITEC City Parking Spot Analogy:

A form checker either says your answer sits in the wrong box (at index) or that the value itself cannot exist — nobody has a thirteenth month.

🔁 How to Reproduce Confirm this is your error

Call LocalDate.parse("2026-13-01") for the invalid-value form and LocalDate.parse("2026/08/22") against ISO_LOCAL_DATE for the at-index form. (Lab capture: OpenJDK 25.)

🛠️ Solutions (5 Ways to Fix)

Solution 1✓ Most common cause

Decode the two message shapes and fix the right layer

👉 Use this when/if the exception arrives from user data or partner feeds and you must decide between fixing data or fixing code.

Invalid value ... : 13 means structure matched but content is impossible — reject or correct the DATA. could not be parsed at index 4 means the text diverged from the PATTERN — fix the formatter (separator, order, locale). One glance at the message tail picks the lane.

if (e.getMessage().contains("at index")) { // wrong shape -> fix formatter/pattern or trim garbage input } else { // invalid value -> fix/reject the data itself }
Solution 2

Defensive boundary parser returning Optional

👉 Use this when/if untrusted strings hit your service and one bad row should not kill the batch.

Wrap parsing in a utility that catches DateTimeParseException and returns Optional.empty() plus a logged warning. Callers decide policy explicitly — no silent defaults, no crashes — and the log keeps the offending text for triage.

static Optional<LocalDate> tryParse(String raw) { try { return Optional.of(LocalDate.parse(raw.trim())); } catch (DateTimeParseException e) { log.warn("Unparseable date [{}]: {}", raw, e.getMessage()); return Optional.empty(); } }
Solution 3

Choose ResolverStyle deliberately (STRICT needs uuuu)

👉 Use this when/if February 29 on non-leap years must fail instead of rolling.

SMART (default) resolves Feb 30 to Feb 28/29-ish silently; STRICT refuses it. The catch: STRICT only works reliably when the pattern uses uuuu (proleptic year), not yyyy (year-of-era). Mixing yyyy with STRICT quietly re-enables surprises.

DateTimeFormatter strict = DateTimeFormatter .ofPattern("dd/MM/uuuu", Locale.ROOT) .withResolverStyle(ResolverStyle.STRICT); LocalDate.parse("30/02/2026", strict); // DateTimeParseException — good
Solution 4

Accept multiple formats with appendOptional chains

👉 Use this when/if legacy feeds send several date conventions and you must parse them all with one entry point.

DateTimeFormatterBuilder.appendOptional stacks alternative sections tried in order; a single formatter then accepts ISO, dd/MM/yyyy, and US styles deterministically without nested try/catch ladders.

DateTimeFormatter multi = new DateTimeFormatterBuilder() .appendOptional(DateTimeFormatter.ISO_LOCAL_DATE) .appendOptional(DateTimeFormatter.ofPattern("dd/MM/uuuu")) .appendOptional(DateTimeFormatter.ofPattern("MM-dd-yyyy")) .toFormatter(Locale.ROOT);
Solution 5

Validate at the edge: converters and Jackson bindings

👉 Use this when/if REST clients keep sending malformed dates and you want clean 400s instead of 500s.

Register a Spring Converter or custom Jackson deserializer that parses once at the boundary and translates DateTimeParseException into a validation error carrying the expected format. Interior code then trusts its LocalDate types completely.

@Component class DateConverter implements Converter<String, LocalDate> { public LocalDate convert(String source) { try { return LocalDate.parse(source); } catch (DateTimeParseException e) { throw new IllegalArgumentException( "Expected yyyy-MM-dd", e); } } }

📋 Version Notes

Java 8

JSR-310 arrives: immutable formatters, three resolver styles, and both message forms born here.

Java 11

Wording stable; CLDR locale-data updates shift some locale-dependent parses after upgrades.

Java 17

Identical behavior; regressions usually come from migrated SimpleDateFormat code that silently rolled bad dates.

Java 21

Unchanged — java.time remains the recommended path over legacy Date/Calendar.

🛡️ How to Prevent This Next Time

Standardize on ISO-8601 for machine interfaces, put ONE parsing utility behind your API boundary, always specify Locale and ResolverStyle explicitly, and unit-test calendar edges (leap years, month boundaries) your users actually hit.