Parsing and formatting dates safely

Why 03/04/2026 is unparseable without a convention, how strict parsers prevent silent errors, and how to emit locale-independent output for machines.

Ambiguous input and lenient parsers

03/04/2026 is 3 April in most of the world and 4 March in the United States. A parser that just works on this input is guessing, and it will guess differently from the parser next to it.

new Date("03/04/2026")       // parsed as March 4 (US, month first)
new Date("2026-03-04")       // parsed as March 4 (ISO, but see the trap below)
new Date("2026-03-04T00:00") // interpreted as LOCAL time, not UTC
new Date("2026-03-04")       // interpreted as UTC midnight

// two forms that look alike produce instants a day apart
  • Date-only strings in JavaScript are treated as UTC; date-time strings without a zone are treated as local.
  • Python's datetime.fromisoformat rejects most non-ISO input, which is a feature.
  • A lenient parser silently shifts a date by a day rather than raising, and the error survives into the database.

Parse strictly, format explicitly

from datetime import datetime, timezone

# explicit format, no guessing
dt = datetime.strptime("2026-09-18 10:30", "%Y-%m-%d %H:%M")
try:
    datetime.strptime("18/09/2026", "%Y-%m-%d")
except ValueError as e:
    print("rejected:", e)      # time data '18/09/2026' does not match format

# machine output: always ISO 8601 with an offset
print(dt.isoformat())                      # 2026-09-18T10:30:00
print(dt.replace(tzinfo=timezone.utc).isoformat())  # ...+00:00
GoalSafe approachAvoid
Accept user inputExplicit format list, reject on mismatchA general-purpose fuzzy parser
Store in a databaseA datetime type, or ISO 8601 text with offsetLocale-formatted strings
Show to a userLocale-aware formatter with a stated zoneManual string concatenation
LogISO 8601 UTCLocal time without a zone
Compare or sortParse to an instant, then compareString comparison of non-ISO formats
from datetime import datetime, timezone
from zoneinfo import ZoneInfo

# a naive datetime is a bug waiting to happen: no zone, no instant
naive = datetime(2026, 9, 18, 10, 30)

# attach a zone explicitly before any conversion
aware = naive.replace(tzinfo=ZoneInfo("Europe/Paris"))
utc = aware.astimezone(timezone.utc)

# subtract aware datetimes; subtracting naive ones across zones is meaningless
delta = datetime.now(timezone.utc) - utc

Formatting for people and for machines

const instant = new Date("2026-09-18T10:30:00Z");

// for a human in a known locale and zone
new Intl.DateTimeFormat("en-GB", {
  dateStyle: "medium", timeStyle: "short",
  timeZone: "Europe/Paris",
}).format(instant);
// "18 Sept 2026, 12:30"

// for a machine: never use toLocaleString
instant.toISOString();   // "2026-09-18T10:30:00.000Z"
💡
Keep two formatting paths: one for humans, which is locale and zone dependent, and one for machines, which is always ISO 8601 in UTC. Mixing them produces log files that cannot be sorted and UI dates that shift by a day near midnight.

FAQ

Why is my date one day off after saving?
Almost always a time-zone conversion on a date-only value. A birthday has no zone; converting it through UTC midnight moves it to the previous day for negative offsets. Store date-only values as date-only types.
Should I use a library for parsing?
Use the platform parser for ISO 8601, and a library when you must accept several human formats. Whichever you choose, list the accepted formats explicitly and reject everything else.

ISO 8601 and RFC 3339 formats Storing dates in databases

Last refreshed 2026-09-18.