Functional patterns

Pitfalls & anti-patterns

A citable index of the warnings scattered across this guide.

Date and time bugs are insidious because they are often silent: the code runs, the value looks plausible, and the error only surfaces when a user in a different time zone reports a problem, or when DST transitions, or when a third-party API quietly changes its format. This page collects the most common anti-patterns as a single citable reference. Each item links to the pitfall callout where it is explained in context.

None of these are exotic edge cases. Every item in this list has caused production incidents in widely-used software.

Common anti-patterns

  • Storing a naive/zoneless time and later assuming UTC.Instant vs civil time
  • Confusing a fixed offset with a named zone. An offset like +02:00 is not a time zone — it does not know about DST transitions. → Time zones vs offsets
  • Assuming every local day has 24 hours / every local time exists. DST gaps and folds violate both assumptions. → DST
  • Measuring elapsed time with the wall clock. The wall clock can jump or go backwards (NTP, DST, user adjustment). Use a monotonic clock for durations. → Clocks
  • Mixing precisions (seconds vs milliseconds). Epoch values in seconds and milliseconds look similar but differ by a factor of 1000. → Precision
  • Trusting a lenient parser with user input. Free-text date strings are locale-dependent and ambiguous; lenient parsers silently guess wrong. → Parsing user input
  • Confusing epoch seconds vs milliseconds from 3rd parties. External APIs do not label their units; assuming the wrong one silently misplaces every timestamp by a factor of 1000 — collapsing recent dates to near the 1970 epoch, or flinging them tens of thousands of years into the future. → 3rd-party data
  • Storing a pure date (or other civil value) as a UTC instant. A birthday or invoice date has no time and no zone; a UTC timestamptz/Date invents both and can shift the date by a day. Use the type that fits the meaning. → Store vs display

Pitfall: The meta anti-pattern — not deciding instant vs civil time up front. Choosing the wrong representation at schema-design time propagates through every layer of an application and is expensive to fix later. Make this decision explicitly before writing your first migration.

Go deeper: why these bugs cluster and how to prevent them systematically

Most of the anti-patterns above share a root cause: implicit assumptions. The wall clock is assumed to be monotonic. The offset is assumed to be stable. The external field is assumed to be UTC. The user’s locale is assumed to match the server’s.

The systematic fix is to make every assumption explicit at the boundary where data enters your system:

  1. Parse strictly at ingestion. Whether the source is a user, an external API, or a database read from a legacy system, parse with an explicit format and fail loudly on mismatch. See Parsing user input and 3rd-party data.
  2. Carry zone information with the value. Never let a zoneless timestamp exist inside your system’s business logic. Attach the zone or UTC marker at ingestion and keep it through to display. See Store vs display.
  3. Agree on a wire format. When data crosses a process boundary, use an unambiguous serialization (RFC 3339 UTC is the safest default) and document it. See Your own client ↔ server.
  4. Test around DST transitions. Unit tests that only run with fixed, non-boundary timestamps will not catch DST bugs. Add test cases for the hour before and after a known transition in your target zones.

The IANA time zone database is the authoritative source for zone rules; keep it up to date in your runtime and infrastructure.


← Back to all topics