On this page
A timestamp, a birthday, and a meeting scheduled in Paris are three different kinds of information. JavaScript's Date can represent an instant, but many application bugs begin when developers use it to represent all three without recording their intent. Choose the meaning first, then the API and storage format.
Separate instants from calendar values
An instant identifies a point on the global timeline. A calendar date such as a birthday has no time or zone. A future appointment may require a local date, a clock time, and a named time zone. Converting everything into midnight UTC can destroy those distinctions before formatting even begins.
Use the Unix timestamp converter to inspect example epoch values and compare their human-readable interpretations. Confirm whether your service sends seconds or milliseconds before writing conversion code. A plausible-looking year is not enough evidence that the units are correct; document the unit in the API field name or schema.
Write down the domain question: are you ordering events, displaying a date printed on a document, or scheduling something according to local rules? This small modeling step is more valuable than memorizing every Date method. The JavaScript and TypeScript category provides related language guidance, but dates always require product decisions as well as syntax.
Understand what Date actually stores
A Date holds a timestamp measured in milliseconds since the Unix epoch, or an invalid numeric value. It does not retain the source time zone. Local getters interpret the instant using the host environment's zone; UTC getters interpret it as UTC. Mixing those families can produce a result that changes across machines.
const seconds = 1_767_571_200;
const instant = new Date(seconds * 1000);
if (Number.isNaN(instant.getTime())) {
throw new Error("Invalid timestamp");
}
console.log(instant.toISOString());
console.log(instant.getUTCFullYear());Use an explicit offset or Z when parsing an instant from an ISO-style string. A date-only string such as 2026-01-05 is interpreted as UTC by Date's standardized parsing rules, while a date-time without an offset is interpreted as local time. That difference explains many unexpected previous-day displays.
Avoid locale-shaped input such as 05/06/2026. It is ambiguous to people and not a portable interchange contract. Read the MDN Date reference for parsing and constructor details. The numeric constructor also uses zero-based months, and overflowing components can normalize rather than fail, so validate user-entered calendar fields separately.
Date objects are mutable. Calling setMonth changes the existing instance, potentially affecting other references. Clone before mutation when a helper should preserve its input, and test month-end behavior rather than assuming that moving from January always produces the same day in February.
Format with Intl instead of assembling strings
Intl.DateTimeFormat combines locale conventions with explicit formatting options. Pass a timeZone when the output must be consistent across viewers, workers, and servers. Otherwise, the environment's zone can turn identical data into different labels, particularly near midnight.
const formatter = new Intl.DateTimeFormat("en-GB", {
dateStyle: "medium",
timeStyle: "short",
timeZone: "Europe/Paris"
});
const label = formatter.format(new Date("2026-01-05T12:00:00Z"));
console.log(label);Cache formatters when formatting many rows with the same options. Use formatToParts when a design needs separately styled components rather than splitting a localized string on spaces or punctuation. Locale output can contain nonbreaking spaces and ordering conventions that a hand-written splitter does not anticipate.
A locale is not a time zone. Choosing French text does not imply that the user is in Paris, and an English-speaking user may work in Tokyo. Store or request the display zone independently when it matters to the workflow. The Intl.DateTimeFormat documentation lists supported options and explains the formatting model.
For a date-only value, avoid converting it through an arbitrary viewer's zone merely to show its year, month, and day. Keep the calendar meaning intact through storage and presentation. This is particularly important for birthdays, invoice dates, and all-day events.
Use Temporal to make intent explicit
Temporal provides separate types for separate meanings: Instant for a timeline point, PlainDate for a calendar date, PlainDateTime for local date-time fields without a zone, and ZonedDateTime for an instant interpreted in a named zone. Duration represents an amount of time with explicit units.
Check native support in every target runtime before relying on it. Availability can differ between browsers, server versions, and embedded webviews. If necessary, use a maintained polyfill and account for its distribution cost. Consult the Temporal documentation rather than assuming every execution environment exposes the same API today.
// With @js-temporal/polyfill installed:
import { Temporal } from "@js-temporal/polyfill";
const birthday = Temporal.PlainDate.from("1992-08-17");
const meeting = Temporal.ZonedDateTime.from({
timeZone: "Europe/Paris",
year: 2026, month: 3, day: 28,
hour: 9, minute: 30
});
const nextDay = meeting.add({ days: 1 });
const next24Hours = meeting.add({ hours: 24 });
console.log(birthday.toString(), nextDay.toString());
console.log(next24Hours.toString());Temporal values are immutable, so addition returns a new value. More importantly, adding one calendar day and adding twenty-four elapsed hours can produce different wall-clock results around a daylight-saving transition. The type system makes the distinction available; your product still has to decide which operation matches the user's intent.
Define scheduling and testing policies
Some local times do not exist when clocks jump forward. Others occur twice when clocks move backward. A booking form needs a disambiguation policy: reject ambiguous input, choose a documented occurrence, or ask the user. Temporal exposes options for this rather than forcing silent guesswork.
Future recurring appointments need special care. A weekly meeting at nine in a named zone is not simply the first instant plus a repeating count of milliseconds. Retain the local schedule and zone, then calculate occurrences using the applicable rules. Governments can change those rules, so avoid promising that a far-future offset is permanent.
Test midnight boundaries, leap days, month ends, invalid input, and at least one daylight-saving gap and overlap. Run tests under more than one host time zone. Freeze the current time when testing relative labels, and avoid exact localized-string assertions unless locale and runtime data are controlled.
For readable interface behavior, pair these checks with the multi-device testing guide. Older webviews deserve explicit attention, especially when introducing Temporal. Serialization contracts should specify units, offsets, and whether a field represents an instant or a calendar value. A field called date is rarely a sufficient contract on its own.
Key takeaways
- Model instants, calendar dates, and zoned schedules separately.
- Check epoch units and reject ambiguous interchange formats.
- Use Intl with deliberate locale and time-zone choices.
- Adopt Temporal with a tested runtime-support strategy.
- Test calendar arithmetic and daylight-saving edge cases explicitly.
Reach designers & developers at work
Put your product next to the tools people use every day. One sponsor per placement, no tracking pixels.
Become a sponsor