Getting the Current Time with Temporal.Now
How to read the current instant, wall-clock time, and civil date with Temporal.Now, and why new Date() gives the wrong day off-box. Part of Modern Temporal API.
Problem framing
It is worth naming the concrete symptoms, because they are the ones that reach a bug tracker. A "good morning / good evening" banner shows the wrong greeting for overseas users because it read the server's hour. A daily-streak feature resets at the server's midnight instead of the user's, so someone who logged in at 11pm local loses their streak. A "posted today" label reads yesterday for a user whose local date has not yet caught up to UTC. A scheduled digest is stamped a day ahead. Every one traces to the same root: a wall-clock reading taken in the server's zone and presented as if it were the user's.
"What time is it?" sounds like one question, but it is really two that legacy code smears together. There is the instant — a single, absolute point on the timeline that is the same everywhere in the universe — and there is the wall-clock reading of that instant, which only exists once you name a place, because the clock on the wall says something different in Tokyo, London, and Chicago at the very same moment. new Date() captures the instant faithfully but then exposes it through the host machine's zone, so the day, hour, and even calendar date you read back depend on where the code happens to run.
That coupling is fine on a developer's laptop, where the host zone is the user's zone, and disastrous on a server, where the host zone is almost always UTC. A greeting that says "good evening," a streak that resets at local midnight, a daily limit, a report labelled with today's date — every one of these reads the wall clock, and every one is silently wrong for a user whose zone differs from the server's during part of the day. Temporal.Now fixes this by refusing to smear the two questions together: it gives you distinct methods for the absolute instant and for the wall-clock reading in a zone you must name.
The mental model to carry through this guide is that the instant is the source of truth and the wall clock is a projection of it. Store and compare instants; compute a wall-clock reading only at the edge where a human will see it, and only in a zone you have chosen on purpose. Everything Temporal.Now offers is organised around making that separation explicit rather than accidental.
"Now" is not a single value — it is an instant, and separately a set of wall-clock fields that only exist once you name a time zone. Legacy new Date() blurs the two: it captures the instant but exposes it through the host machine's zone, so the same code reports a different calendar day on a server in Los Angeles and a browser in Tokyo. Temporal.Now splits the concept into precise methods, forcing you to say whether you want the absolute instant or the current wall-clock reading in a named zone.
API reference
A useful way to remember the surface is by what each method omits. instant() omits the zone and the calendar fields — it is pure absolute time. plainDateISO(zone) omits the time-of-day — it is a pure civil date. plainTimeISO(zone) omits the date — it is a pure wall-clock time. zonedDateTimeISO(zone) omits nothing — it is the full picture, instant plus zone plus calendar. Picking the method is picking exactly the information you want and no more, which keeps accidental data (a stray time-of-day, a lurking zone) from tipping a later comparison.
Temporal.Now is a small namespace of read-only clocks, each returning the present moment as a different type. Temporal.Now.instant() gives the absolute instant with nanosecond resolution and no zone — the direct analogue of an epoch timestamp, and the right choice for logging, ordering, and anything you will store. Temporal.Now.zonedDateTimeISO(timeZone) attaches an IANA zone to that instant to produce a full wall-clock value, offset and all.
For the common "what is today" question there is Temporal.Now.plainDateISO(timeZone), which returns the current civil date in a named zone as a zoneless PlainDate — exactly the shape you want when you mean the day rather than a moment within it. There is also plainDateTimeISO and plainTimeISO for the wall-clock date-and-time or time alone, and Temporal.Now.timeZoneId(), which reports the host's own IANA identifier when you genuinely want to know where the code is running.
The naming is deliberate: every method that produces a wall-clock reading takes a timeZone argument, so you cannot accidentally get a local value without having named the locale it is local to. That is the API encoding the lesson — there is no zero-argument "what time is it locally" that silently reaches for the host zone, because that silent reach is the bug this whole API exists to prevent.
Temporal.Now is a namespace of read-only clocks. Each returns the current moment as a different type.
Approach A: legacy Date
The deeper lesson is that Date makes the wrong thing easy and the right thing hard: the terse call reads the host zone, and getting a specific zone requires ceremony. Temporal.Now inverts that incentive — the terse call still makes you name a zone — which is why adopting it tends to eliminate a whole category of "works on my machine, wrong in production" date bugs rather than just patching individual instances.
To see the trap concretely, consider new Date().toISOString().slice(0, 10), a popular one-liner for "today's date." It returns the UTC calendar date, so for a user in California late in the evening it is already tomorrow's date, and for a user in Sydney early in the morning it can be yesterday's. The code looks zone-neutral because it uses toISOString, but UTC is a zone like any other, and it is almost never the user's.
With legacy Date, reading the current instant is fine: Date.now() returns epoch milliseconds, which is host-independent and correct. The trouble is everything built on top of it. new Date() gives you an object whose field accessors — getHours, getDate, getMonth — all read through the host zone, so "the current hour" or "today" is whatever those are on the machine, not for any particular user.
There is no way with Date alone to ask "what is the current date in Tokyo" without constructing an Intl.DateTimeFormat with an explicit timeZone and parsing its output, which is awkward enough that developers reach for the host-zone accessors instead and ship the bug. The API pushes you toward the wrong default, and the wrong default only misbehaves for a fraction of users during a fraction of the day, so it survives testing.
Legacy code also tends to conflate Date.now() (milliseconds) with the whole-second Unix time that many surrounding systems use, adding a units hazard on top of the zone hazard. The modern approach separates these concerns cleanly: an instant is an instant, a wall-clock reading names its zone, and the unit of an epoch value is explicit in the accessor you choose.
Legacy code reads the clock with Date.now() (epoch ms) or new Date() (an instant viewed through the host zone). There is no way to ask for "today in Tokyo" without constructing a formatter.
const nowMs = Date.now(); // absolute, fine
const today = new Date().getDate(); // host-zone day — wrong off-box
Approach B: Temporal.Now
There is a pleasing symmetry worth noticing: the same instant flows into every method, and each method is just a different lens on it. Temporal.Now.instant() is the raw light; zonedDateTimeISO(zone) puts a zone-shaped lens on it; plainDateISO(zone) puts a date-shaped lens on it. Because they all start from the one instant, values taken at the same moment are mutually consistent, and you can always convert a zoned reading back to the instant it came from without loss.
The Temporal approach is to name what you want. Need the absolute moment for a log line or a database timestamp? Temporal.Now.instant(). Need to show the user their local time? Temporal.Now.zonedDateTimeISO(userZone). Need today's date to compute a streak or a daily limit? Temporal.Now.plainDateISO(userZone). Each call says, in its name and its argument, exactly which of the two questions it is answering and in whose frame.
Because the zone is an explicit argument resolved against the IANA database, daylight saving is handled for you: the offset in effect right now is applied automatically, so "the current time in America/New_York" is correct whether that is currently EST or EDT, with no arithmetic on your part. You never touch a raw offset, which is the thing that goes stale twice a year.
The types reinforce correct downstream handling. A PlainDate from plainDateISO cannot be accidentally re-zoned because it has no zone; an Instant from instant() cannot be accidentally displayed in the wrong local time because it has no wall-clock fields until you attach a zone. Choosing the type is choosing the guarantees, which is why matching the method to the question is most of the battle.
Name the zone and Temporal returns the correct wall-clock present.
import { Temporal } from '@js-temporal/polyfill';
const instant = Temporal.Now.instant(); // absolute
const tokyoNow = Temporal.Now.zonedDateTimeISO('Asia/Tokyo'); // wall clock in Tokyo
const tokyoToday = Temporal.Now.plainDateISO('Asia/Tokyo'); // civil date in Tokyo
Production implementation
A small but valuable refinement is to expose the current-time helpers as named, unit- and zone-explicit functions rather than scattering Temporal.Now calls through the codebase. A module that offers now() returning an Instant, todayIn(zone) returning a PlainDate, and localNow(zone) returning a ZonedDateTime gives every caller a clear, correct entry point, centralises the fallback-to-UTC decision, and provides the single seam you later swap for a test clock. Centralisation also means that when a requirement changes — say, defaulting an unknown zone to a company timezone rather than UTC — you change it in one place.
For serverless and edge deployments this discipline is not optional. A function may run in any region, its host zone effectively random from the user's perspective, so any code path that reads a wall-clock value without an explicit zone is a latent bug that manifests differently depending on where the request landed. Injecting the clock and threading the user's zone turns that non-determinism into a controlled input, so the same request produces the same result no matter which edge node served it.
The single most valuable production pattern is to stop calling Temporal.Now directly inside business logic and instead inject the clock. Define a tiny Clock interface — an object with a now() returning an Instant, and perhaps a today(zone) returning a PlainDate — wire the real implementation in production, and pass a fixed implementation in tests. Time-dependent code becomes deterministic without any global patching.
The second pattern concerns the user's zone. The server cannot know it, so resolve it once on the client with Intl.DateTimeFormat().resolvedOptions().timeZone, persist it against the user's account or in a cookie, and thread it into every Temporal.Now.*ISO(zone) call thereafter. On a first render before the client has reported in, default to UTC as an explicit, documented fallback rather than letting the host zone leak in by accident.
The third is to keep instants as the canonical representation and derive wall-clock readings only at the presentation edge. A record's created_at is an instant; the string "today at 3pm" is a projection computed when rendering for a specific user. Holding that discipline means the same data renders correctly for every user in every zone, and comparisons and ordering — which should always be on the absolute instant — never accidentally depend on someone's wall clock.
Inject the clock so code is testable rather than reading Temporal.Now directly everywhere.
import { Temporal } from '@js-temporal/polyfill';
export interface Clock { now(): Temporal.Instant; today(tz: string): Temporal.PlainDate; }
export const systemClock: Clock = {
now: () => Temporal.Now.instant(),
today: (tz) => Temporal.Now.plainDateISO(tz),
};
// Pass a fixed Clock in tests for deterministic results.
Edge cases
Leap seconds are a final curiosity worth knowing about even though they rarely matter in application code: civil timekeeping and the platform clock smear or ignore them, so Temporal.Now.instant() will not hand you a 61st second, and you should not write logic that expects one. If you ever need an authoritative, leap-aware time source you are into specialist territory well beyond what the browser or Node clock provides.
A subtle edge case is the user who travels or whose device zone changes mid-session. If you cached their zone at login, a flight from New York to London can leave "today" computed against a stale zone. Re-reading Intl.DateTimeFormat().resolvedOptions().timeZone on load, and updating the persisted value when it changes, keeps the current-time features aligned with where the user actually is.
The host zone is the first edge case: on a server it is usually UTC, and Temporal.Now.timeZoneId() will tell you what it actually is, which is occasionally surprising in containerised or serverless environments where it may be configured explicitly. Never assume it matches any user.
Clock resolution and monotonicity are the second. Temporal.Now.instant() can report nanoseconds, but the underlying platform clock may be coarser, and more importantly it follows the wall clock, which can jump backward when the system time is corrected. For measuring elapsed time use performance.now(), which is monotonic; reserve Temporal.Now.instant() for timestamps that represent an actual moment.
The third is the date-boundary window. For a few hours around midnight, the current date differs between zones, so "today" computed in the server's zone and "today" computed in the user's zone can disagree by a day. This is not a bug to work around but a fact to respect: compute "today" in the zone whose day you actually mean, which for user-facing features is the user's.
Gotchas & common pitfalls
It is also easy to forget that Temporal.Now.timeZoneId() reports the host zone, not the user's — it answers "where is this code running," which is occasionally useful for diagnostics but is exactly the value you must not use as a stand-in for the user's zone. Reaching for it to decide how to display a time to a user reintroduces the host-zone bug through the back door.
One more pitfall catches teams adopting Temporal incrementally: mixing Date.now() and Temporal.Now.instant() in the same comparison. They measure the same timeline but in different units and types, so comparing them requires an explicit conversion. Pick one clock source for a given piece of logic — ideally the Temporal one — and convert at the boundary if a legacy API hands you the other.
The headline pitfall is reading the current date or hour from new Date() on a server and treating it as the user's — it is the UTC value, wrong for a slice of every day. Use Temporal.Now.plainDateISO(userZone) or zonedDateTimeISO(userZone) instead.
A second is using wall-clock timestamps to measure durations; because the wall clock can jump, a duration from two Date.now() readings can be wrong or negative. Use a monotonic clock for timing. A third is calling Temporal.Now deep inside logic, which makes the code untestable and forces global stubbing; inject the clock so time is a controllable input.
A fourth is deriving a zone from a raw numeric offset, which cannot survive a DST transition and cannot distinguish the many zones that share an offset. Always work from the IANA identifier, and let Temporal.Now resolve the current offset for you.
Testing checklist
Taken together, these checks encode the whole philosophy of the page: the instant is the source of truth, every wall-clock reading names its zone, and time enters the system as an injected input rather than an ambient global. A suite that holds those three invariants will catch the host-zone and untestable-clock bugs long before they reach production.
Round out the suite with a guard that no production code path calls a zero-argument wall-clock reader that would fall back to the host zone. A simple lint rule or grep for host-zone accessors (getHours, getDate, getMonth on a bare new Date()) in application code catches the regression at review time, before it reaches the fraction of users and the fraction of the day where it would otherwise hide.
It also pays to snapshot the behaviour at a known instant across a spread of zones in a single table-driven test: for a frozen Instant, assert the expected plainDateISO result for UTC, a far-western zone, and a far-eastern zone in one place. That table becomes living documentation of the date-boundary behaviour and catches any regression that reintroduces a host-zone dependency.
Time-dependent tests are only trustworthy when time is an input, so the first thing to verify is that your code accepts an injected clock and that a fixed clock produces a fixed result. Assert that today(zone) returns the expected civil date for a frozen instant, and that two far-apart zones can yield dates a day apart for the same instant.
Cover the host-independence property by asserting that logic which should depend only on the injected clock gives identical results under several process TZ values — any variation means a host-zone read has leaked in. Add a case that an invalid zone identifier throws, and that an unknown user zone falls back to your documented default.
Finally, if any feature advances through time — a countdown, a scheduler, an expiry — test it with a controllable clock you step forward by a Temporal.Duration, asserting state at successive moments without real waiting. The same injectable seam that freezes time lets you fast-forward it, which is what makes these features deterministic to test.
Frequently Asked Questions
How do I get the current date in a specific time zone with Temporal?
Call Temporal.Now.plainDateISO(timeZone) with an IANA id such as 'Asia/Tokyo'. It returns today's civil date in that zone, which can differ from the server's date, so it is the reliable way to compute 'today' for a user in another region.
Is Temporal.Now.instant() suitable for measuring elapsed time?
No. Like Date.now() it reads the wall clock, which can jump when the system time is adjusted. For measuring durations use performance.now(), which is monotonic; use Temporal.Now.instant() for timestamps that represent an actual moment.
How do I make code that reads the current time testable?
Wrap Temporal.Now behind a small Clock interface and inject it. Production wires the system clock; tests pass a clock that returns a fixed instant, so time-dependent logic becomes deterministic without monkey-patching globals.
What is the difference between Temporal.Now.instant() and zonedDateTimeISO()?
instant() returns the absolute moment with no zone — use it for logging, ordering, and storage. zonedDateTimeISO(timeZone) attaches an IANA zone to that same instant to produce a wall-clock reading with an offset — use it to show a user their local time. One is the point on the timeline; the other is how a particular place reads that point.
How do I get the current date for a user in another time zone?
Call Temporal.Now.plainDateISO(timeZone) with the user's IANA identifier. It returns today's civil date in that zone as a zoneless PlainDate, correctly accounting for the current daylight-saving offset, which can differ from the server's date for users far from UTC during part of the day.
How do I make code that reads the current time testable?
Inject the clock. Give functions a now parameter defaulting to Temporal.Now.instant(), or pass a small Clock object; in tests supply a fixed instant. The logic then depends on an input you control instead of the real wall clock, so time-dependent behaviour becomes deterministic without patching globals.