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.

Timestamp to Date decision flowA flow that checks whether an incoming timestamp is in seconds or milliseconds, scales seconds by 1000, constructs a Date, and formats it with an explicit timeZone.timestampseconds?know sourcevalue × 1000to millisecondsvalue as-isnew Date(ms)+ explicit timeZoneyesno (ms)

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

new Date(1700000000);        // WRONG: 1970-01-20T...Z (treated as 1.7M ms)
new Date(1700000000 * 1000); // RIGHT: 2023-11-14T22:13:20.000Z
date.toString();                                   // WRONG: host-zone dependent
new Intl.DateTimeFormat('en-US', { timeZone: 'America/New_York' }).format(date); // RIGHT

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.