W3docs

Java Date Formatting

Format Java dates and times into strings with DateTimeFormatter and standard or custom patterns.

Java Date Formatting

Every java.time type has a toString() that produces the ISO-8601 representation: 2025-11-04, 14:30:00, 2025-11-04T14:30:00Z. That's fine for logs and machine-to-machine traffic. For human-facing display ("November 4, 2025" or "4 Nov, 14:30") you need a formatter.

The class is java.time.format.DateTimeFormatter. It is the modern, thread-safe, immutable replacement for java.text.SimpleDateFormat. Cache one as a static final and reuse it across threads forever — no synchronisation, no defensive copying.

Three ways to build a formatter

// 1. Built-in ISO formatters
DateTimeFormatter.ISO_LOCAL_DATE;                  // 2025-11-04
DateTimeFormatter.ISO_LOCAL_DATE_TIME;             // 2025-11-04T14:30:00
DateTimeFormatter.ISO_OFFSET_DATE_TIME;            // 2025-11-04T14:30:00-05:00
DateTimeFormatter.ISO_ZONED_DATE_TIME;             // 2025-11-04T14:30:00-05:00[America/New_York]
DateTimeFormatter.ISO_INSTANT;                     // 2025-11-04T19:30:00Z
DateTimeFormatter.BASIC_ISO_DATE;                  // 20251104

// 2. Localised formatters
DateTimeFormatter.ofLocalizedDate(FormatStyle.LONG);          // November 4, 2025 (en-US)
DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM);    // Nov 4, 2025, 2:30:00 PM

// 3. Pattern-based formatters
DateTimeFormatter.ofPattern("dd MMM yyyy");                   // 04 Nov 2025
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm zzz");          // 2025-11-04 14:30 EST

The pattern API is the one you'll use most. The localised one is right when you need a culture-appropriate format and want the JDK to choose the layout for you.

Formatting

The call shape is symmetric on both sides:

String s = formatter.format(temporal);
String s2 = temporal.format(formatter);                       // same thing, fluent style

Both work. Most code uses the fluent form.

LocalDate today = LocalDate.now();
String us = today.format(DateTimeFormatter.ofPattern("MM/dd/yyyy"));         // 11/04/2025
String iso = today.format(DateTimeFormatter.ISO_LOCAL_DATE);                 // 2025-11-04
String eu = today.format(DateTimeFormatter.ofPattern("dd.MM.yyyy"));          // 04.11.2025

The pattern alphabet

The big table — the one you'll come back to. Letters are case-sensitive and the count matters.

LetterMeaningExample
yyeary2025, yy25, yyyy2025
MmonthM11, MM11, MMMNov, MMMMNovember
dday of monthd4, dd04
Eday of weekETue, EEEETuesday
Hhour 0-23H14, HH14
hhour 1-12h2, hh02 (use with a)
aAM/PMaPM
mminutem5, mm05
sseconds9, ss09
Sfraction of secondSSS123 (millis)
nnanosecondnnnnnnnnn123456789
zzone namezEST, zzzzEastern Standard Time
Zzone offsetZ-0500, ZZ-0500, ZZZZGMT-05:00
XISO offsetX-05, XX-0500, XXX-05:00
Vzone IDVVAmerica/New_York

Literal text wraps in single quotes:

DateTimeFormatter.ofPattern("EEEE, MMMM d 'at' h:mm a");      // Tuesday, November 4 at 2:30 PM

For a literal single quote, use two: ''.

The most-confused pair is m vs M (lowercase = minute, uppercase = month) and H vs h (uppercase = 0-23, lowercase = 1-12). Most "the time is off by something weird" bugs come from one of these typos.

Localisation: Locale and withLocale

A formatter picks up the JVM default locale unless you tell it otherwise. For "always in English" or "always in German" output, pin the locale:

DateTimeFormatter english = DateTimeFormatter.ofPattern("EEEE, MMMM d", Locale.US);
DateTimeFormatter german  = DateTimeFormatter.ofPattern("EEEE, d. MMMM", Locale.GERMAN);
DateTimeFormatter french  = DateTimeFormatter.ofPattern("EEEE d MMMM", Locale.FRENCH);

today.format(english);   // Tuesday, November 4
today.format(german);    // Dienstag, 4. November
today.format(french);    // mardi 4 novembre

For server-rendered content, always pass a locale. The "default locale of the JVM" is unpredictable on production servers and the source of "looks right on my laptop, wrong on the server" bugs.

Time zone display

ZonedDateTime and Instant are the only types that have zone information. Formatting a LocalDateTime with a pattern that includes z or Z throws — there's no zone to print. Convert first:

ZonedDateTime zdt = ldt.atZone(ZoneId.of("America/New_York"));
zdt.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm z"));   // 2025-11-04 14:30 EST

For Instant, the formatter needs a zone too — Instant has none, so display formatters that include any zone-dependent field need a withZone:

DateTimeFormatter f = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")
    .withZone(ZoneId.of("America/New_York"));
f.format(Instant.now());                                      // formatter supplies the zone for display

Without withZone, formatting an Instant with a calendar-shaped pattern throws.

Stylized formatters with FormatStyle

The localised factories give you four canonical sizes:

DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT);   // 11/4/25 (en-US), 04.11.25 (de-DE)
DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM);  // Nov 4, 2025
DateTimeFormatter.ofLocalizedDate(FormatStyle.LONG);    // November 4, 2025
DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL);    // Tuesday, November 4, 2025

Same four sizes exist for ofLocalizedTime and ofLocalizedDateTime. Use these when you want the layout to follow the user's locale rather than enforcing one shape. Pair them with .withLocale(...) to pin the locale.

A worked example: one date, six display variants

The program below formats one ZonedDateTime in six common ways: ISO for machine logs, US 12-hour for English users, European 24-hour for German users, a long localised form, a custom pattern with embedded literal text, and an Instant-via-withZone formatter for raw timestamps.

java— editable, runs on the server

What to take from the run:

  • The cached static final DateTimeFormatter fields are the right shape. DateTimeFormatter is immutable and thread-safe; creating one is cheap but not free, and reusing the same instance everywhere is the JDK-recommended pattern. Don't construct a fresh one inside a hot loop.
  • The same ZonedDateTime produced six different strings depending on the formatter. The value object never changed; the formatter is the only thing that controls layout. That's the separation DateTimeFormatter exists for — keep the value type clean, push presentation to the formatter.
  • The "common typo" block printed 14:11 for HH:MM because M is month, not minute. The two are the most-confused pair in the pattern alphabet. If your displayed time looks suspiciously like a date component, look at the pattern's case.
  • The FormatStyle ladder produced four progressively-longer strings. Use FormatStyle.MEDIUM as a sensible default for "show a date to a user without thinking too hard"; LONG and FULL for context where the year and the day of week need to be unambiguous; SHORT for tight UI spaces.
  • LocalDateTime with a zone-bearing pattern threw — the formatter needs zone data, and LocalDateTime doesn't have any. The fix is to convert (ldt.atZone(zone)) or to drop the zone-bearing field from the pattern. Either way, the failure mode is clear at runtime.

What's next

Formatting is the value → string direction. The next chapter, Java Date Parsing, is the inverse — string → value — using the same DateTimeFormatter patterns and the same set of caveats. The two together are the I/O boundary for any code that exchanges dates with users, configs, logs, or remote APIs.

Practice

Practice

A web server logs timestamps with `ZonedDateTime.now().format(DateTimeFormatter.ofPattern('yyyy-MM-dd HH:mm'))`. On a German-locale JVM the month gets printed as 'Nov' instead of '11'. What's the most likely cause?