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:
- 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.
- 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=UTCin dev and test environments so implicit-zone bugs surface immediately. - 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. - 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.