A service holds timestamps as epoch milliseconds and must display each one in a chosen time zone. How do you render that with Intl.DateTimeFormat, and why is slicing or regexing the formatted string to rearrange it a bug?
answer
- the instant is zone-agnostic
- zone chosen at format time
- styles and components cannot be mixed
- the output is locale data, not a fixed pattern
- typed parts instead of string surgery
basics
~20 sPass a timeZone option with an IANA identifier to Intl.DateTimeFormat and format the timestamp; the zone affects only rendering. Never parse the output — component order, separators and digits vary by locale. Use formatToParts to assemble a custom layout from typed pieces.
solid answer
~40 sConstruct `new Intl.DateTimeFormat(locale, { timeZone: 'America/New_York', dateStyle: 'medium', timeStyle: 'short' })` and call `format(ms)`. The timestamp is an instant; the `timeZone` option decides which wall-clock rendering of that instant you see, and an unknown zone identifier throws a RangeError. `Intl.DateTimeFormat().resolvedOptions().timeZone` tells you the environment's own zone, which is what you get if you pass nothing — fine in a browser, usually wrong on a server. Parsing the formatted string is fragile because everything about it is locale data: component order (`11/5/2024` vs `05/11/2024`), separators, day-period markers, non-Latin digits, and non-breaking spaces you cannot see. `formatToParts()` returns an array of `{ type, value }` objects — `year`, `month`, `day`, `literal` — so you can build any layout you need from labelled pieces instead of guessing at offsets.
code
javascript · 10 linesconst ms = 1700000000000;
const nyc = new Intl.DateTimeFormat('en-US', {
timeZone: 'America/New_York',
dateStyle: 'medium',
timeStyle: 'short',
});
console.log(nyc.format(ms)); // "Nov 14, 2023, 5:13 PM"
console.log(Intl.DateTimeFormat().resolvedOptions().timeZone); // host zone idgo deeper
Know that Intl.DateTimeFormat takes a timeZone option with an IANA name like 'Europe/Berlin', and that the same timestamp renders differently per zone without changing the underlying value.
Explain that styles and component options are mutually exclusive and that mixing them throws, that an unknown zone throws a RangeError, and that resolvedOptions().timeZone reports the environment's zone.
Show why parsing formatted output is a defect — order, separators, digits and invisible spacing are all locale data — and reach for formatToParts. Be ready to say where the display zone comes from and why a server default is wrong.
Own the contract: instants travel through storage and APIs, rendering happens at the edge, and the user's zone preference is data the system carries so that app pages, exports and notifications agree.
## The shape of the API ```js const fmt = new Intl.DateTimeFormat('en-US', { timeZone: 'America/New_York', dateStyle: 'medium', timeStyle: 'short', }); fmt.format(1700000000000); // "Nov 14, 2023, 5:13 PM" ``` `format` accepts a number of epoch milliseconds or a `Date`. The value identifies an *instant*; the `timeZone` option chooses which wall clock that instant is expressed on. Nothing about the input changes — rendering in Tokyo and in New York are two views of the same number. ## The timeZone option It takes `'UTC'` or an IANA identifier such as `'Europe/Berlin'`. An unrecognized identifier throws a **RangeError**, so validate before constructing if the value is user-supplied. Omit the option and you inherit the host's zone — read it explicitly when you need to know: ```js Intl.DateTimeFormat().resolvedOptions().timeZone; // e.g. "Europe/Berlin" ``` That is the correct way to discover the environment's zone. In a browser it is a reasonable default for the viewer; in a server process it is the container's configuration and has nothing to do with any user, which is why server-rendered timestamps must carry an explicit zone. A useful companion in ES2023 runtimes is `Intl.supportedValuesOf('timeZone')`, which returns the identifiers the runtime supports — handy for populating a picker instead of shipping a hardcoded list. ## Two option families that do not mix There are two ways to say what you want rendered: 1. **Styles** — `dateStyle` and `timeStyle`, each `'full' | 'long' | 'medium' | 'short'`. Locale-idiomatic and terse. 2. **Components** — `weekday`, `year`, `month`, `day`, `hour`, `minute`, `second`, `fractionalSecondDigits`, plus `timeZoneName` and `hour12` / `hourCycle`. Combining a style with any individual component option throws a **TypeError**. This trips people upgrading a formatter: adding `second: '2-digit'` to a `timeStyle` formatter does not refine it, it breaks it. Pick one family. `timeZoneName` accepts `'short'` and `'long'`, and in ES2022 runtimes also `'shortOffset'`, `'longOffset'`, `'shortGeneric'` and `'longGeneric'` — the offset forms are what you want when the label must be unambiguous. ## Why parsing the output is a defect Everything visible in the string is locale data: - **Order** differs — month/day/year in `en-US`, day/month/year in `en-GB`, year-first elsewhere. - **Separators** differ — `/`, `.`, `-`, or a localized word. - **Digits** differ — some locales default to a non-Latin numbering system. - **Spaces** are not always U+0020 — narrow no-break spaces appear around day-period markers and in grouped numbers, and the exact choice has changed between ICU data versions. So `out.split('/')[0]` is correct on your machine and wrong on a user's, and `/(\d+):(\d+) (AM|PM)/` fails on a string whose space is invisible-but-different. The intended escape hatch is `formatToParts`: ```js const parts = new Intl.DateTimeFormat('en-GB', { timeZone: 'UTC', year: 'numeric', month: '2-digit', day: '2-digit', }).formatToParts(Date.UTC(2024, 0, 5)); // [{type:'day',value:'05'}, {type:'literal',value:'/'}, ...] const get = (t) => parts.find((p) => p.type === t).value; `${get('year')}-${get('month')}-${get('day')}`; // "2024-01-05" ``` You get labelled pieces regardless of the locale's ordering, and you can drop, reorder or wrap any of them — which is also how you put a `<time>`-style span around just the day, or grey out the year. ## Ranges `formatRange` and `formatRangeToParts` render two instants as one idiomatic range ("5–8 Jan 2024"), collapsing the shared components the way the locale expects. Building that by formatting twice and joining with a dash produces "5 Jan 2024 – 8 Jan 2024", which no locale considers idiomatic. ## Cost and reuse Construction resolves locale and time-zone data and is the expensive step; `format` on an existing instance is cheap. A table of timestamps should build one formatter, not one per row. Cache by a key combining locale, zone and a stable serialization of the options. ## What stays out of the presentation layer Store and transmit the instant — epoch milliseconds or an ISO 8601 string with an offset — and format only at the point of display. A formatted timestamp in a database or an API payload is a rendering decision frozen into data: it cannot be re-rendered in another zone, cannot be compared, and cannot be sorted reliably.
- What happens if you pass both dateStyle and an individual option such as day: '2-digit'?The constructor throws a TypeError. `dateStyle` and `timeStyle` form one option family and the individual components form another; they cannot be combined. This surprises people refining an existing formatter, since adding a component reads like a tweak but is actually a hard error. Choose the component family whenever you need fine control.
- How would you build a stable "YYYY-MM-DD" string for a specific time zone?Use `formatToParts` with `year: 'numeric', month: '2-digit', day: '2-digit'` and the target `timeZone`, then read the parts by `type` and join them yourself. That is locale-order independent. Pair it with a locale that guarantees Latin digits, such as `'en-CA'` or the `'en-u-nu-latn'` tag, so a non-Latin default numbering system cannot leak into a machine-readable string.
- Where should the display time zone come from in a multi-user product?From an explicit user preference, falling back to the browser's resolved zone via `Intl.DateTimeFormat().resolvedOptions().timeZone`, and never from the server's own configuration. Store the preference so server-rendered pages and emails match what the user sees in the app; a server default silently renders everyone's timestamps in the container's zone.
saying these in an interview costs you the question
- Assuming the formatted string always has a fixed component order
- Regexing format() output to extract the year or hour
- Using the server's default zone for user-facing timestamps
- Mixing dateStyle with individual component options
- Storing formatted timestamps instead of the instant