Fix: DateFormatter Returns nil When Converting a Date String in Swift

Fix: DateFormatter Returns nil When Converting a Date String in Swift

Written by

in

Quick answer: date(from:) returns nil because a formatter’s dateFormat must match the input string exactly, including the time portion. Use one DateFormatter with yyyy-MM-dd HH:mm:ss to parse the string into a Date, then a second formatter with MMM d, yyyy to produce the output text.

Why this happens

A DateFormatter has a single dateFormat pattern, and that pattern is used for both directions: turning a string into a Date and turning a Date back into a string. When you set dateFormat = "MM-dd-yyyy" and then hand it "2016-02-29 12:24:26", the formatter tries to read the string as month-day-year and finds a year first, plus a time component it was never told about. Parsing fails and date(from:) returns nil.

The pattern is not a hint or a loose template. Every character in the input that carries meaning must be represented by a matching token, and every token must be present in the input. Separators matter too: a space, a colon, a comma, or a literal T all have to line up.

Two smaller issues cause the same nil in practice. First, fixed-format dates are parsed using the formatter’s locale and calendar, so a device set to a non-Gregorian calendar or a locale with different month names can fail unexpectedly. Second, if the input has no time zone, the formatter’s timeZone decides how the wall-clock time is interpreted, which can shift the result by hours even when parsing succeeds.

How to fix it

1. Use two formatters: one to parse, one to print

The accepted fix is to stop trying to do both jobs with one object. Parse the original string with a formatter that matches it exactly, then format the resulting Date with a second formatter that matches the output you want.

import Foundation

let input = "2016-02-29 12:24:26"

let parser = DateFormatter()
parser.locale = Locale(identifier: "en_US_POSIX")
parser.dateFormat = "yyyy-MM-dd HH:mm:ss"

let printer = DateFormatter()
printer.locale = Locale.current
printer.dateFormat = "MMM d, yyyy"

guard let date = parser.date(from: input) else {
    print("There was an error decoding the string")
    return
}

print(printer.string(from: date)) // Feb 29, 2016

Note the difference between d and dd. MMM d, yyyy prints Feb 9, 2016 for a single-digit day, while MMM dd, yyyy prints Feb 09, 2016. Pick the one you actually want instead of copying a pattern blindly.

2. Compare the pattern to the input token by token

Most nil results come from one wrong token. Walk through the string from left to right and confirm each piece has a matching pattern element.

Input piece Pattern token
2016 yyyy
02 MM
29 dd
12 (24-hour) HH
24 mm
26 ss
Feb MMM
February MMMM
Monday EEEE
2:11 PM h:mm a
+0000 Z
.112 SSS

Common mix-ups: MM is month but mm is minutes; HH is 24-hour but hh is 12-hour and needs an a token for AM/PM; ss is seconds while SSS is fractional seconds. A literal character that also happens to be a pattern letter, such as the T in an ISO 8601 string, must be wrapped in single quotes: yyyy-MM-dd'T'HH:mm:ssZ.

3. Handle the optional instead of force-unwrapping

date(from:) is failable by design. Force-unwrapping it turns a bad pattern into a crash in production.

if let date = parser.date(from: input) {
    print(printer.string(from: date))
} else {
    // Log the raw string and the pattern you used.
    print("Failed to parse \(input) with \(parser.dateFormat ?? "nil")")
}

Logging both the input and the pattern is the fastest way to spot a mismatch, because the two strings sit next to each other in the console.

4. For user-facing output, skip dateFormat entirely

If the goal is to show a date to a person, a hard-coded pattern ignores their locale and 12/24-hour preference. Use a style-based formatter or the modern formatting API instead.

let printer = DateFormatter()
printer.dateStyle = .medium
printer.timeStyle = .none
print(printer.string(from: date)) // Feb 29, 2016 (en_US)

On iOS 15 and later you can format directly on the Date:

print(date.formatted(date: .abbreviated, time: .omitted))

Keep dateFormat for machine-readable strings, logs, and API payloads where the exact shape matters.

5. Use ISO8601DateFormatter for ISO 8601 input

If the source string looks like 2018-09-12T14:11:54+0000, don’t hand-build a pattern. ISO8601DateFormatter already knows the rules.

let iso = ISO8601DateFormatter()
iso.formatOptions = [.withInternetDateTime]

if let date = iso.date(from: "2018-09-12T14:11:54+0000") {
    print(date)
}

If the string includes fractional seconds, add .withFractionalSeconds to the options. Mixing the two shapes with one configuration will return nil, so decide which shape you receive before parsing.

How to prevent it

  • Keep one DateFormatter per distinct pattern. Never reuse a parser as a printer.
  • Set locale = Locale(identifier: "en_US_POSIX") on any formatter that uses a fixed dateFormat, so device settings can’t change the result.
  • Set timeZone explicitly when the input string has no zone of its own.
  • Treat date(from:) as failable everywhere and log the input plus the pattern on failure.
  • Cache formatters instead of creating them inside tight loops; DateFormatter creation is expensive.
  • Add a unit test with a single-digit day, a 12-hour time, and a string with a trailing time zone.

FAQ

Why does date(from:) return nil when my format looks correct?

Check for invisible differences: a leading or trailing space, a non-breaking space, a comma where the pattern expects a space, or a literal T that isn’t quoted. Also confirm the input really uses 24-hour time if your pattern says HH.

What is the difference between HH and hh?

HH reads a 24-hour clock from 00 to 23. hh reads a 12-hour clock from 01 to 12 and normally needs an a token to consume the AM/PM marker. Using hh on 14:11 fails because 14 is not a valid 12-hour value.

Can I convert the string directly to another string without a Date?

Not reliably. Date is the bridge between the two formats: parse into a Date, then format that Date. Slicing and rearranging the characters yourself breaks as soon as the input shape changes.

Sources & credits

Parts of this guide are adapted from content licensed under CC BY-SA 4.0; this article is shared under the same license.