Date & Time API

Formatting & Parsing

15 min Lesson 6 of 13

Formatting & Parsing

Every application that surfaces dates to a user — or reads them from external input — must convert between java.time objects and strings. Java's answer is DateTimeFormatter, a thread-safe, immutable class that replaces the old SimpleDateFormat. In this lesson you will learn how to use the predefined formatters, build your own patterns, and handle locale-sensitive output correctly.

Why DateTimeFormatter Is Better Than SimpleDateFormat

The old SimpleDateFormat is infamous for two problems: it is not thread-safe (shared instances cause data races) and it belongs to the messy java.util.Date world. DateTimeFormatter fixes both — it is immutable and interoperates natively with java.time. You should never reach for SimpleDateFormat in new code.

Thread safety matters. A single DateTimeFormatter instance can be stored in a static final field and reused concurrently across threads with no synchronisation. A shared SimpleDateFormat will silently corrupt output under concurrency.

Predefined Formatters

The DateTimeFormatter class ships with a set of ready-made constants for the most common use cases. These require no configuration and produce well-known formats.

import java.time.LocalDate; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; LocalDate date = LocalDate.of(2025, 9, 15); LocalDateTime dt = LocalDateTime.of(2025, 9, 15, 14, 30, 45); // ISO 8601 — the go-to for APIs and storage System.out.println(date.format(DateTimeFormatter.ISO_LOCAL_DATE)); // 2025-09-15 System.out.println(dt.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)); // 2025-09-15T14:30:45 System.out.println(dt.format(DateTimeFormatter.ISO_INSTANT)); // compile error — needs a ZonedDateTime // RFC 1123 — used in HTTP headers (requires ZonedDateTime) import java.time.ZonedDateTime; import java.time.ZoneOffset; ZonedDateTime zdt = ZonedDateTime.of(dt, ZoneOffset.UTC); System.out.println(DateTimeFormatter.RFC_1123_DATE_TIME.format(zdt)); // Mon, 15 Sep 2025 14:30:45 GMT
Prefer ISO 8601 for inter-system communication. When writing to a database, message queue, or REST API always use ISO_LOCAL_DATE_TIME or ISO_INSTANT. Locale-sensitive formats like "Sep 15, 2025" belong only at the presentation layer.

Custom Patterns with ofPattern()

When a predefined formatter does not match your requirement, use DateTimeFormatter.ofPattern(String pattern). The pattern language is similar to the old SimpleDateFormat but slightly stricter and more consistent.

Key pattern letters (case matters):

  • y — year; yyyy for four-digit, yy for two-digit
  • M / MM / MMM / MMMM — month as number, two-digit, abbreviated name, full name
  • d / dd — day of month
  • H / HH — hour in day (0–23)
  • h / hh — hour in AM/PM (1–12); pair with a (AM/PM marker)
  • m / mm — minute; s / ss — second
  • E / EEEE — abbreviated / full day-of-week name
  • z — time zone name; Z — offset (+0000); xxx — offset with colon (+00:00)
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm"); LocalDateTime dt = LocalDateTime.of(2025, 9, 15, 14, 30); // formatting String s = dt.format(fmt); // "15/09/2025 14:30" System.out.println(s); // parsing LocalDateTime parsed = LocalDateTime.parse("01/03/2026 09:00", fmt); System.out.println(parsed); // 2026-03-01T09:00
Month letters M vs. minute letters m — always lowercase m for minutes. Writing MM:ss instead of mm:ss will silently format the month number where you intended the minute. This is an extremely common typo.

Locale-Sensitive Formatting

Pattern-based formatters are locale-neutral — MMMM always uses the JVM's default locale unless you say otherwise. Use DateTimeFormatter.ofPattern(pattern, locale) or the withLocale() method to produce locale-aware output.

import java.util.Locale; LocalDate date = LocalDate.of(2025, 9, 15); DateTimeFormatter enFmt = DateTimeFormatter.ofPattern("EEEE, MMMM d, yyyy", Locale.ENGLISH); DateTimeFormatter arFmt = DateTimeFormatter.ofPattern("EEEE، d MMMM yyyy", Locale.forLanguageTag("ar")); DateTimeFormatter deFmt = DateTimeFormatter.ofPattern("EEEE, d. MMMM yyyy", Locale.GERMAN); System.out.println(date.format(enFmt)); // Monday, September 15, 2025 System.out.println(date.format(arFmt)); // الاثنين، 15 سبتمبر 2025 System.out.println(date.format(deFmt)); // Montag, 15. September 2025

FormatStyle — High-Level Localised Formats

When you want locale-aware output without specifying a pattern, use DateTimeFormatter.ofLocalizedDate(FormatStyle). The JDK resolves the correct pattern for the locale at runtime.

import java.time.format.FormatStyle; LocalDate date = LocalDate.of(2025, 9, 15); DateTimeFormatter shortEn = DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT).withLocale(Locale.ENGLISH); DateTimeFormatter mediumEn = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM).withLocale(Locale.ENGLISH); DateTimeFormatter longEn = DateTimeFormatter.ofLocalizedDate(FormatStyle.LONG).withLocale(Locale.ENGLISH); DateTimeFormatter fullEn = DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL).withLocale(Locale.ENGLISH); System.out.println(date.format(shortEn)); // 9/15/25 System.out.println(date.format(mediumEn)); // Sep 15, 2025 System.out.println(date.format(longEn)); // September 15, 2025 System.out.println(date.format(fullEn)); // Monday, September 15, 2025
Prefer FormatStyle in UI code. Hard-coding a pattern like "MM/dd/yyyy" silently breaks for users in locales where that order is wrong. FormatStyle.SHORT.withLocale(userLocale) adapts automatically without code changes.

Parsing and Error Handling

Both LocalDate.parse(text, formatter) and formatter.parse(text) throw DateTimeParseException (a RuntimeException) when the input does not match the pattern. Always handle this at the boundary where untrusted text enters your application.

import java.time.format.DateTimeParseException; DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd"); String input = "not-a-date"; try { LocalDate d = LocalDate.parse(input, fmt); } catch (DateTimeParseException e) { System.out.println("Invalid date: " + e.getMessage()); // "Invalid date: Text 'not-a-date' could not be parsed at index 0" }

DateTimeFormatterBuilder for Complex Formats

Sometimes you need formats that cannot be expressed as a simple pattern string — optional sections, literal text, or multiple fallback patterns. DateTimeFormatterBuilder lets you compose a formatter programmatically.

import java.time.format.DateTimeFormatterBuilder; import java.time.temporal.ChronoField; // A formatter that accepts both "yyyy-MM-dd" and "yyyy-MM-dd HH:mm" DateTimeFormatter flexible = new DateTimeFormatterBuilder() .appendPattern("yyyy-MM-dd") .optionalStart() .appendLiteral(' ') .appendPattern("HH:mm") .optionalEnd() .parseDefaulting(ChronoField.HOUR_OF_DAY, 0) .parseDefaulting(ChronoField.MINUTE_OF_HOUR, 0) .toFormatter(); LocalDateTime a = LocalDateTime.parse("2025-09-15 14:30", flexible); // with time LocalDateTime b = LocalDateTime.parse("2025-09-15", flexible); // defaults to 00:00 System.out.println(a); // 2025-09-15T14:30 System.out.println(b); // 2025-09-15T00:00
parseDefaulting is essential when a pattern does not contain a time component but you are parsing into a LocalDateTime. Without it, the parse will fail because the time fields are absent.

Reusing Formatters — the Static Final Pattern

Because DateTimeFormatter is immutable and thread-safe, declare shared instances as static final constants. Constructing a new formatter inside every method call adds unnecessary allocation and makes the codebase harder to change centrally.

public final class DateFormats { public static final DateTimeFormatter ISO_DATE = DateTimeFormatter.ISO_LOCAL_DATE; public static final DateTimeFormatter DISPLAY = DateTimeFormatter.ofPattern("d MMM yyyy", Locale.ENGLISH); public static final DateTimeFormatter API_DATETIME = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss", Locale.ROOT); private DateFormats() {} // utility class — no instances }

Summary

DateTimeFormatter is the single, thread-safe tool for all date/time string conversion in modern Java. Use predefined constants for machine formats (ISO 8601, RFC 1123), ofPattern for fixed custom layouts, and ofLocalizedDate with a FormatStyle when the output must adapt to the user's locale. Store reusable formatters in static final constants, and always catch DateTimeParseException at the boundary between external text and your domain model.