Safeguard
Open Source

date-fns-tz: Time Zone Handling and Package Health Review

A practical review of the npm date-fns-tz package: how it handles IANA time zones, how healthy the project is, and when date-fns v4's built-in time zone support replaces it.

Yukti Singhal
Platform Engineer
6 min read

The npm date-fns-tz package is the established companion library that adds IANA time zone support to date-fns — and as of date-fns v4, it is no longer the only option, because the core project now ships first-party time zone support through @date-fns/tz. That fork in the road is the most important thing to understand before adopting it in 2025: date-fns-tz remains a solid, widely used choice for date-fns v2/v3 codebases, while greenfield projects on date-fns v4 should evaluate the first-party packages before reaching for the third-party one. This review covers what date-fns-tz actually does, its API after the v3 renames, the health signals that matter, and a migration decision framework.

What problem it solves

JavaScript's Date is a UTC timestamp wearing a local-time costume: it has no time zone of its own, and every formatting call implicitly uses the machine's zone. The npm date-fns library gives you excellent immutable date arithmetic but historically inherited that limitation. date-fns-tz fills the gap using the Intl.DateTimeFormat API built into every modern runtime — meaning no bundled time zone database, which keeps the package small and means zone data stays current with the runtime rather than with your package-lock.json.

The three functions you'll use constantly:

import { formatInTimeZone, toZonedTime, fromZonedTime } from "date-fns-tz";

// Render a UTC instant in a specific zone
formatInTimeZone(new Date("2025-03-08T18:30:00Z"), "America/New_York", "yyyy-MM-dd HH:mm zzz");
// -> "2025-03-08 13:30 EST"

// "What wall-clock time is this instant in Tokyo?"
const tokyoWall = toZonedTime(someUtcDate, "Asia/Tokyo");

// "This wall-clock time in Berlin — what UTC instant is it?"
const utcInstant = fromZonedTime("2025-06-01 09:00", "Europe/Berlin");

If you're reading older tutorials: version 3 renamed utcToZonedTime to toZonedTime and zonedTimeToUtc to fromZonedTime. The old names described the direction confusingly, and a lot of Stack Overflow answers still use them — check which major you're on before copy-pasting.

Package health: the signals that matter

Evaluated the way we'd evaluate any dependency before adoption:

  • Adoption: millions of weekly downloads on npm and a large dependent count. You will not be the person who finds the basic bugs.
  • Release cadence: measured rather than frantic. The 3.x line (current as of this writing, with 3.2.0 the latest) tracks date-fns v3 compatibility. Slow releases in a scoped utility library are a neutral-to-good signal — the problem domain is stable.
  • Security history: no published CVEs against date-fns-tz itself to date. Its attack surface is genuinely small: it transforms dates and format strings, takes no network input, and runs no install scripts.
  • Dependency weight: it peers against date-fns and adds essentially nothing else, so it doesn't drag a subtree into your SBOM.
  • Bus factor: like most utility libraries, a small maintainer group. That's the honest risk line item — the same one that applies to a large share of the npm registry, and the reason inventorying maintainer concentration across your whole tree (something an SCA platform can automate) beats worrying about any single package.

Net: healthy, low-risk, correctly scoped. The open question isn't quality — it's positioning, now that upstream moved.

The date-fns v4 fork in the road

In September 2024, date-fns v4 shipped with first-class time zone support via the first-party @date-fns/tz package. Its approach differs from date-fns-tz: instead of converter functions, it provides TZDate — a drop-in Date subclass that performs all calculations in a given zone — plus a minimal TZDateMini variant around 1.3 KB:

import { TZDate } from "@date-fns/tz";
import { addHours, format } from "date-fns";

const meeting = new TZDate(2025, 5, 1, 9, 0, "Europe/Berlin");
format(addHours(meeting, 3), "HH:mm"); // arithmetic stays in Berlin time

Because every date-fns v4 function accepts these objects natively, zone-awareness stops being a wrapper step and becomes a property of the value itself. That's a cleaner model for code where zones flow through many operations.

Decision framework:

  • On date-fns v2/v3 already using date-fns-tz: stay. It works, it's maintained, and migration buys you little.
  • Starting fresh on v4: use @date-fns/tz. First-party support, value-level zone semantics, tiny footprint.
  • Migrating a large codebase v3 → v4: the date-fns v4 release notes emphasize minimal breaking changes; the time zone layer will be most of your migration diff. Budget for it deliberately rather than mixing both models long-term — two zone abstractions in one codebase is how off-by-one-zone bugs are born.

Operational pitfalls that cause real bugs

These bite regardless of which package you choose:

  1. Store UTC, convert at the edge. Persist instants (UTC ISO strings or epoch millis) and apply zones only when formatting for a human or interpreting their input. The moment "zoned" dates enter your database, comparisons and arithmetic degrade into guesswork.
  2. Server zone drift. Code that works locally because your laptop is in the user's time zone and breaks in CI (which runs UTC) is the classic date-fns-tz-adjacent bug. Set TZ=UTC in dev and test environments so implicit-zone bugs surface immediately.
  3. DST boundaries. fromZonedTime("2025-03-09 02:30", "America/New_York") names a wall-clock time that never existed (the spring-forward gap). Decide explicitly how your product handles nonexistent and ambiguous times; don't let the library's default decide for you.
  4. Old runtimes and Hermes. The implementation leans on Intl; very old Node versions or trimmed ICU builds can produce wrong zone output rather than loud errors. Pin runtime versions and test a couple of exotic zones (Asia/Kathmandu, Australia/Lord_Howe) in CI.

When you do upgrade across majors — v2 to v3's function renames especially — run it through a controlled dependency-upgrade flow rather than a blind bump; our npm-check-updates workflow covers a pattern that catches rename breakage in minutes.

FAQ

Is date-fns-tz still maintained?

Yes. The 3.x line is current, tracks date-fns v3, and the project has an active repository. Its long-term positioning is the thing to watch now that date-fns v4 ships first-party zone support upstream.

Does date-fns-tz bundle its own time zone database?

No — that's one of its best properties. It uses the runtime's Intl API, so zone data (including boundary and DST rule changes) updates with your Node.js or browser version instead of requiring a package release. (One caveat: your runtime's ICU data must be complete — use node builds with full ICU.)

Should new projects use date-fns-tz or @date-fns/tz?

New projects on date-fns v4 should default to @date-fns/tz and its TZDate model. Choose date-fns-tz when you're on date-fns v2/v3 or when a dependency in your stack already standardizes on it.

Are there security concerns with date and time libraries?

Rarely direct ones — date-fns-tz has no CVE history and minimal attack surface. The realistic risks are supply-chain generic: typosquats of popular names and compromised maintainer accounts, which lockfiles, provenance checks, and dependency scanning address across the board.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.