Convert a Unix Timestamp to a Date in JavaScript
To convert a Unix timestamp to a Date, multiply seconds by 1000 (or pass milliseconds straight through) into new Date(ms), then format with an explicit timeZone. Part of Unix Timestamps & Epoch Conversion.
Why This Is Trickier Than It Looks
The conversion itself is one line; the bugs live in the unit and the display. JavaScript's new Date() interprets a single numeric argument as milliseconds, but the timestamp you received almost certainly came from a seconds-based source — a JWT, a Postgres extract(epoch …), a git commit date, a Stripe webhook. Pass those raw seconds in and you get a valid Date pointing at January 1970, with no error to catch.
The second failure mode is display. A Date is an absolute instant; calling .toString() renders it in the host machine's timezone. On a developer laptop that looks fine; on a UTC-by-default serverless host the same code prints a different wall-clock time. The conversion is only correct when you pin the output zone explicitly.
Minimal Working Solution
If you already know the unit — and you should, because guessing is fragile — the conversion is direct.
const epochSeconds = 1700000000;
// new Date() takes milliseconds, so scale a seconds value by 1000.
const date = new Date(epochSeconds * 1000);
console.log(date.toISOString()); // "2023-11-14T22:13:20.000Z" (UTC, no host-zone drift)
Milliseconds need no scaling:
const epochMs = 1700000000000;
const date = new Date(epochMs); // already milliseconds
Full Production Version
Production code validates the input, accepts the unit explicitly, and exposes a typed formatter that always pins the zone. toISOString() is the right choice for machine output; Intl.DateTimeFormat (with a cached, zone-pinned instance) is the right choice for display.
type EpochUnit = 'seconds' | 'milliseconds';
function unixToDate(value: number, unit: EpochUnit): Date {
if (!Number.isFinite(value)) {
throw new TypeError(`Timestamp must be a finite number, got ${value}`);
}
// Normalise to milliseconds; the single-arg Date constructor expects ms.
const ms = unit === 'seconds' ? value * 1000 : value;
const date = new Date(ms);
if (Number.isNaN(date.getTime())) {
throw new RangeError(`Timestamp ${value} produced an Invalid Date`);
}
return date;
}
// Cache the formatter: constructing Intl.DateTimeFormat is expensive.
const nyFormatter = new Intl.DateTimeFormat('en-US', {
timeZone: 'America/New_York', // explicit zone — never rely on the host
dateStyle: 'medium',
timeStyle: 'long',
});
const date = unixToDate(1700000000, 'seconds');
console.log(nyFormatter.format(date)); // "Nov 14, 2023, 5:13:20 PM EST"
Temporal expresses the same conversion with the unit named in the constructor, then attaches a zone for display so the absolute instant renders as local wall time:
import { Temporal } from '@js-temporal/polyfill';
// fromEpochSeconds names the unit, so there is no ambiguity about scale.
const instant = Temporal.Instant.fromEpochSeconds(1700000000);
// Attach an IANA zone to turn the absolute instant into wall-clock time.
const zoned = instant.toZonedDateTimeISO('America/New_York');
console.log(zoned.toString()); // "2023-11-14T17:13:20-05:00[America/New_York]"
Verification Snippet
import { Temporal } from '@js-temporal/polyfill';
// 1. Seconds and milliseconds resolve to the SAME instant after scaling.
console.assert(
new Date(1700000000 * 1000).getTime() === new Date(1700000000000).getTime(),
'scaled seconds must equal the millisecond value',
);
// 2. Display is host-zone independent: pin the zone and the output is fixed.
const fmt = (tz: string) =>
new Intl.DateTimeFormat('en-GB', { timeZone: tz, timeStyle: 'short', hour12: false })
.format(new Date(1700000000000));
console.assert(fmt('UTC') === '22:13', 'UTC display must be 22:13 regardless of host');
console.assert(fmt('Asia/Tokyo') === '07:13', 'Tokyo is UTC+9 — next morning');
// 3. Temporal accessor round-trips back to the original seconds value.
const instant = Temporal.Instant.fromEpochSeconds(1700000000);
console.assert(instant.epochSeconds === 1700000000, 'epochSeconds must round-trip');
Common Pitfalls
- Passing seconds where milliseconds are expected.
new Date(1700000000); // WRONG: 1970-01-20T...Z (treated as 1.7M ms)
new Date(1700000000 * 1000); // RIGHT: 2023-11-14T22:13:20.000Z
- Letting the host zone decide the displayed time.
date.toString(); // WRONG: host-zone dependent
new Intl.DateTimeFormat('en-US', { timeZone: 'America/New_York' }).format(date); // RIGHT
- Using
toLocaleString()with notimeZoneon the server. Same problem astoString()— output drifts with the deploy environment. Always passtimeZone.
Frequently Asked Questions
Why does my converted date show 1970?
You passed a seconds value into new Date(), which expects milliseconds. 1700000000 is interpreted as ~1.7 million milliseconds — about 20 days past the epoch. Multiply the seconds value by 1000 first.
How do I display the date in a specific timezone?
Construct the Date from the epoch value, then format with Intl.DateTimeFormat passing an explicit timeZone such as 'America/New_York', or use Temporal.Instant.toZonedDateTimeISO(zone). Never rely on the host machine's zone, especially on serverless hosts.