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 one-line answer — new Date(epochSeconds * 1000) — hides the single most common Unix-timestamp bug: the units. JavaScript's Date constructor and Date.now() work in milliseconds since the epoch, but a great many systems — Unix date +%s, most SQL extract(epoch …), many APIs and log formats — emit seconds. Feed a seconds value straight into new Date() and you get a moment in January 1970, because the number is a thousand times too small; feed a milliseconds value into a seconds-expecting function and you land 50,000 years in the future. The values look plausible enough individually that the mistake often ships.

The deeper trap is that there is no reliable way to infer the unit from the number alone in the general case, so the unit has to be known and declared at the boundary where the value enters your system. A timestamp is just an integer; whether it means seconds or milliseconds is metadata that lives in the source's documentation, not in the value. Getting the conversion right is therefore less about the arithmetic and more about pinning down and asserting the unit at the point of ingestion.

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

Convert epoch seconds to a DateMultiply epoch seconds by 1000 before constructing a DateSeconds in, milliseconds expected1710513000epoch SECONDS× 1000new Date(ms)→ Mar 2024 ✓Feeding the raw seconds value skips 1970 → 1973 and lands in January 1970.

Full Production Version

The robust pattern is to convert at the boundary and to make the unit explicit in the code that does it. A function named fromEpochSeconds(n) that returns new Date(n * 1000) and a fromEpochMillis(n) that returns new Date(n) document the unit in their names, so a call site cannot silently apply the wrong scaling. Doing the scaling once, at ingestion, means the rest of the system works in a single consistent representation (a Date or a Temporal Instant) and never has to re-guess. If you use Temporal, Temporal.Instant.fromEpochMilliseconds(n) and fromEpochSeconds-style helpers play the same role with the same discipline.

Two production details matter. First, timestamps have no zone — they are pure instants — so converting one to a displayed local time is a separate step that requires choosing a zone (toZonedDateTimeISO(zone)), and conflating the conversion with the display is how zone bugs sneak in. Second, be explicit about precision: seconds-based timestamps lose sub-second detail, and if your source is milliseconds you should not multiply as if it were seconds. Pinning the unit and the zone as deliberate, named steps — rather than a single opaque new Date(x) — is what makes epoch conversion reliable across the many systems that disagree about units.

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]"

Unit-detecting converterDetect seconds vs milliseconds by magnitude, then build a DateUnit-detecting converternumeric inputdigits ≤ 10?→ secondsnormalizeto msnew Date(ms)validate

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');

Epoch conversion assertionsSeconds and milliseconds both resolve to the same instantAssertions that prove the edge casetoDate(1710513000)2024-03-15T14:30ZtoDate(1710513000000)2024-03-15T14:30ZtoDate(0)1970-01-01T00:00Znegative epochpre-1970

Common Pitfalls

The number-one pitfall is unit confusion: passing a seconds value to new Date() (which expects milliseconds), yielding a 1970 date, or the reverse, yielding a far-future one. Scale explicitly at the boundary and name the unit in your conversion functions. The second is trying to auto-detect the unit from the magnitude of the number, which is unreliable — the ranges overlap for plausible dates — so treat the unit as known metadata from the source rather than something to guess. The third is forgetting that a timestamp carries no zone, and reading local components off the resulting Date as if the timestamp "knew" a zone; choose the display zone explicitly.

A fourth pitfall is precision loss going unnoticed: a source that is actually milliseconds, multiplied by 1000 as though it were seconds, produces a wildly wrong instant, while a nanosecond source truncated to milliseconds silently drops detail. Confirm the source's unit and precision at ingestion. Finally, when displaying, remember that toISOString() shows UTC — which is correct and host-independent for a canonical representation, but is not the user's local time, so for a user-facing display you must project into the user's zone first.

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

Epoch conversion pitfallsForgetting the seconds-to-ms conversion or double-multiplyingWrongRightnew Date(1710513000)≈ 20 Jan 1970new Date(seconds * 1000)correct instant× 1000 twiceyear 56 000name the unit (epochSeconds)no double convert

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.

How do I convert a Unix timestamp to a Date in JavaScript?

If the timestamp is in seconds, scale it: new Date(epochSeconds * 1000). If it is already in milliseconds, pass it directly: new Date(epochMs). The Date constructor expects milliseconds, so the whole task hinges on knowing the source's unit. Convert at the boundary with a unit-named helper like fromEpochSeconds, and for a local display, project the resulting instant into a zone rather than reading host-local components.

Why does my Unix timestamp convert to 1970 or a far-future date?

Because of a units mismatch. new Date() expects milliseconds, but many systems (Unix date +%s, SQL extract(epoch), various APIs) emit seconds. Passing a seconds value directly makes it a thousand times too small, landing in January 1970; passing a milliseconds value where seconds are expected lands tens of thousands of years ahead. Scale explicitly — multiply seconds by 1000 — and confirm the source's unit at ingestion.

Does a Unix timestamp include a time zone?

No. A Unix timestamp is a pure instant — a count from the epoch — with no zone attached. Converting it to a displayed local time is a separate step that requires choosing a zone, e.g. instant.toZonedDateTimeISO(zone). toISOString() shows the instant in UTC, which is correct as a canonical form but is not the user's local time, so project into the user's zone for a user-facing display.