☕ Java
DateTimeFormatter
DateTimeFormatter is the java.time class responsible for converting temporal objects (LocalDate, LocalTime, LocalDateTime, ZonedDateTime, and others) to and from formatted text, replacing the non-thread-safe SimpleDateFormat from the old date-time API. This entry covers DateTimeFormatter's immutability and thread-safety as a deliberate design improvement, how to build custom formatters from pattern letters, the built-in ISO and localized formatter constants available out of the box, and how parsing failures and locale-sensitivity affect formatter behavior in practice.
Immutability and Thread-Safety — The Core Improvement Over SimpleDateFormat
DateTimeFormatter is immutable and thread-safe by design, meaning a single DateTimeFormatter instance can be safely shared across multiple threads, cached as a static final field, and reused indefinitely without any risk of one thread's formatting operation corrupting or interfering with another's — this is a direct and deliberate fix for the single most notorious problem with the old SimpleDateFormat, which was mutable internally and consequently not thread-safe at all, meaning a SimpleDateFormat instance shared across threads without external synchronization could silently produce corrupted or incorrect output under concurrent use, a bug class responsible for a great deal of subtle, hard-to-reproduce production issues in pre-Java-8 codebases.
Because DateTimeFormatter carries no mutable internal state that formatting or parsing operations could corrupt, the recommended and idiomatic pattern is to create formatter instances once — typically as static final constants — and reuse the same instance everywhere a particular format is needed throughout an application, in sharp contrast to the old defensive pattern of creating a brand-new SimpleDateFormat instance for every single use (or wrapping every use in a ThreadLocal) purely to avoid the thread-safety hazard.
Java
// ── OLD — SimpleDateFormat is NOT thread-safe, sharing it is a real bug ─
// private static final SimpleDateFormat OLD_FORMAT =
// new SimpleDateFormat("yyyy-MM-dd"); // DANGEROUS if shared across threads —
// concurrent calls to .format()/.parse() can corrupt each other's internal state
// ── NEW — DateTimeFormatter is immutable, safe to share freely ────────
public class DateUtils {
public static final DateTimeFormatter ISO_DATE_FORMAT =
DateTimeFormatter.ofPattern("yyyy-MM-dd"); // created ONCE, reused everywhere
}
// Many threads can safely call this concurrently — no shared mutable state exists:
String formatted1 = LocalDate.now().format(DateUtils.ISO_DATE_FORMAT);
String formatted2 = LocalDate.of(2024, 1, 1).format(DateUtils.ISO_DATE_FORMAT);Building Custom Formatters with Pattern Letters
DateTimeFormatter.ofPattern(patternString) is the most common way to build a custom formatter, using a defined set of pattern letters where each letter (or repeated run of the same letter) represents a specific field and a specific minimum width or style — yyyy for a four-digit year, MM for a zero-padded two-digit month, dd for a zero-padded two-digit day, HH for a zero-padded 24-hour-clock hour, mm for minutes, ss for seconds, and so on, with the exact count of repeated letters often controlling the output style (M produces "6", MM produces "06", MMM produces "Jun", and MMMM produces "June" — the same underlying field, rendered at increasing levels of verbosity purely based on how many pattern letters are used).
A frequent point of confusion is the case-sensitivity and overlap of certain pattern letters that look similar but mean genuinely different fields — lowercase mm is minutes-of-hour, while uppercase MM is month-of-year, and mixing the two up is a common, easy-to-make mistake that produces a formatter which compiles and runs without error but silently displays the wrong field entirely; similarly, lowercase yyyy (year) is distinct from uppercase YYYY (week-based-year, an ISO-8601 concept that can differ from the calendar year for dates near the start/end of the year), and the two are not interchangeable despite looking nearly identical.
Java
LocalDateTime dt = LocalDateTime.of(2024, 6, 5, 14, 9, 3);
// ── Repeated pattern letters control verbosity of the SAME field ──────
System.out.println(dt.format(DateTimeFormatter.ofPattern("M"))); // 6
System.out.println(dt.format(DateTimeFormatter.ofPattern("MM"))); // 06
System.out.println(dt.format(DateTimeFormatter.ofPattern("MMM"))); // Jun
System.out.println(dt.format(DateTimeFormatter.ofPattern("MMMM"))); // June
// ── A FULL custom pattern, combining several fields ────────────────────
DateTimeFormatter full = DateTimeFormatter.ofPattern("EEEE, MMMM d, yyyy 'at' hh:mm a");
System.out.println(dt.format(full)); // Wednesday, June 5, 2024 at 02:09 PM
// ── The mm vs MM trap — easy to swap by mistake, no compile error ─────
DateTimeFormatter wrong = DateTimeFormatter.ofPattern("yyyy-mm-dd"); // mm = MINUTES, not month!
System.out.println(dt.format(wrong)); // 2024-09-05 — "09" is the MINUTE field, NOT June!
DateTimeFormatter correct = DateTimeFormatter.ofPattern("yyyy-MM-dd");
System.out.println(dt.format(correct)); // 2024-06-05 — MM correctly gives the monthBuilt-In Formatter Constants, Locale, and Parsing Failures
DateTimeFormatter provides a number of pre-built static constants for the most common standardized formats, sparing application code from having to hand-write a pattern for them — ISO_LOCAL_DATE, ISO_LOCAL_TIME, and ISO_LOCAL_DATE_TIME cover the standard ISO-8601 representations with no offset, while ISO_DATE, ISO_TIME, and ISO_DATE_TIME are offset/zone-aware variants for when that information is present on the object being formatted. Separately, DateTimeFormatter.ofLocalizedDate(FormatStyle.LONG) (and its ofLocalizedTime/ofLocalizedDateTime counterparts, each accepting a FormatStyle of SHORT, MEDIUM, LONG, or FULL) produce formatters whose actual output text is locale-sensitive, automatically rendering in the date/time conventions appropriate to either the JVM's default Locale or one explicitly supplied via .withLocale(Locale), useful for displaying dates in a way that matches a user's own regional conventions rather than a single hardcoded pattern.
Whether using a custom pattern or a built-in constant, calling .parse(text) (whether directly via DateTimeFormatter.parse() or indirectly via LocalDate.parse(text, formatter) and similar) on text that does not match the formatter's expected pattern throws DateTimeParseException, with an exception message and error index specifically identifying where in the input string parsing actually failed — this is a checked-feeling but actually unchecked exception, meaning code calling parse() on any input that isn't already guaranteed to be well-formed (such as raw user input) should generally wrap the call in a try/catch rather than letting a malformed string propagate as an uncaught runtime exception.
Java
LocalDate date = LocalDate.of(2024, 6, 15);
// ── Built-in ISO constants — no need to hand-write the pattern ────────
System.out.println(date.format(DateTimeFormatter.ISO_LOCAL_DATE)); // 2024-06-15
// ── Locale-sensitive formatting — output adapts to the Locale supplied
DateTimeFormatter longUS = DateTimeFormatter.ofLocalizedDate(FormatStyle.LONG)
.withLocale(Locale.US);
DateTimeFormatter longFR = DateTimeFormatter.ofLocalizedDate(FormatStyle.LONG)
.withLocale(Locale.FRANCE);
System.out.println(date.format(longUS)); // June 15, 2024
System.out.println(date.format(longFR)); // 15 juin 2024 — SAME date, locale-appropriate text
// ── Parsing failures — DateTimeParseException, NOT silently ignored ───
try {
LocalDate bad = LocalDate.parse("15-06-2024"); // wrong order for default ISO format
} catch (DateTimeParseException e) {
System.out.println("Failed at index " + e.getErrorIndex() + ": " + e.getMessage());
}Related Topics in Date Time API
LocalDate
LocalDate is the java.time class (introduced in Java 8 as part of the redesigned date-time API replacing the deprecated java.util.Date/Calendar) representing a date without a time component or a time zone — just a year, month, and day on the ISO-8601 calendar. This entry covers why LocalDate deliberately carries no time-of-day or zone information, how it is created and parsed, how it is manipulated via its immutable, fluent with*/plus*/minus* methods, how two LocalDate instances are compared and the period between them measured, and the common pitfalls of treating it as if it secretly carried a time or zone after all.
LocalTime
LocalTime is the java.time class representing a time-of-day without any date or time zone attached — just an hour, minute, second, and nanosecond on a 24-hour clock. This entry covers what LocalTime intentionally omits and why, how to create and parse LocalTime values with varying precision, how arithmetic on LocalTime wraps around midnight instead of rolling into a different day, and how LocalTime is meant to be combined with LocalDate (via LocalDateTime) rather than used as a date substitute.
LocalDateTime
LocalDateTime is the java.time class combining a LocalDate and a LocalTime into a single date-and-time-of-day value that still, deliberately, carries no time zone or UTC-offset information. This entry covers why LocalDateTime is best understood as a composite of the two simpler types rather than a primitive type in its own right, how it is created from and decomposed back into its LocalDate/LocalTime parts, why the absence of a zone makes it unsuitable for representing one unambiguous, real-world instant, and how it relates to and converts into the zone-aware types (ZonedDateTime, Instant) when an actual moment in time is what's needed.
ZonedDateTime
ZonedDateTime is the java.time class representing a LocalDateTime combined with a ZoneId, fully resolved against the time zone's rules to also carry a specific UTC offset — making it the type capable of representing one genuinely unambiguous real-world instant while still preserving human-readable, zone-local wall-clock fields. This entry covers the relationship between ZoneId and the resolved offset, how daylight saving transitions create gaps and overlaps that ZonedDateTime must resolve, how zone-aware arithmetic differs from plain LocalDateTime arithmetic across those transitions, and when to prefer ZonedDateTime over the simpler offset-only OffsetDateTime or the zone-independent Instant.