~ / topics / Languages
$man lang-javascript-and-typescript

JavaScript and TypeScript

In brief: JavaScript's Date counts milliseconds in a 64-bit floating-point number and works until the year 275760, so the language has no 2038 problem of its own. Bugs come from tricks that squeeze numbers into 32 bits: the popular | 0 shortcut, Int32Array buffers, and binary formats. Code that runs on 32-bit devices or reads data from C programs can also be affected.

Risk level Low for Date. Medium for code using bitwise operators, typed arrays or binary protocols
Status Language is safe. Pitfalls are in application code and libraries
Last reviewed 2026-10-21

2038 status at a glance

Not affected or different deadline (1), and why

Overview

The ECMAScript spec defines a time value as a Number, an IEEE-754 double, holding integral milliseconds since 1970-01-01T00:00:00Z. The valid range is clipped to ±8.64 × 10¹⁵ ms (±100,000,000 days), which is "the range from April 20, 271821 BC to September 13, 275760 AD". Anything outside it becomes an Invalid Date (NaN) [1]. Doubles represent integers exactly up to 2⁵³, so millisecond precision holds across the whole range.

new Date(8.64e15).toISOString();   // "+275760-09-13T00:00:00.000Z"
new Date(8.64e15 + 1).getTime();   // NaN

(Both lines tested with Node.js on 2026-10-21.)

TypeScript adds no runtime representation. A number is still a double, so TypeScript's type checker cannot tell a 32-bit-safe number from an unsafe one.

Technical details

Pitfall 1: bitwise operators truncate to signed 32-bit

Every bitwise operator (|, &, ^, ~, <<, >>) first converts its Number operands to 32-bit integers. MDN warns: "Bitwise ORing any number x with 0 returns x converted to a 32-bit integer, which additionally removes leading bits for numbers outside the range -2147483648 to 2147483647. Use Math.trunc() instead" [2].

// BUG: popular "fast floor" idioms
const nowSec  = Date.now() / 1000 | 0;
const nowSec2 = ~~(Date.now() / 1000);
const nowSec3 = (Date.now() / 1000) >> 0;

// At 2038-01-19T03:14:08Z:
(Date.UTC(2038, 0, 19, 3, 14, 8) / 1000) | 0;          // -2147483648
Math.floor(Date.UTC(2038, 0, 19, 3, 14, 8) / 1000);    // 2147483648   (FIX)

// >>> 0 gives an *unsigned* 32-bit value: survives 2038 but wraps on 2106-02-07
(Date.now() / 1000) >>> 0;

(Outputs verified with Node.js on 2026-10-21.) Minifiers and older "performance tips" spread | 0 widely, so search bundled third-party code as well as your own.

Pitfall 2: Int32Array and other typed arrays

Int32Array "represents an array of 32-bit signed integers" [3]. When a value is stored, it goes through the ToInt32 conversion, which wraps modulo 2³²:

const buf = new Int32Array(1);
buf[0] = 2 ** 31;          // 2038-01-19T03:14:08Z in seconds
buf[0];                    // -2147483648  (BUG)

const ok = new BigInt64Array(1);   // FIX: or Float64Array
ok[0] = BigInt(Math.floor(Date.now() / 1000));

The same applies to DataView.setInt32(), Buffer.writeInt32LE()/writeInt32BE() in Node, and WebAssembly i32 values that cross the JS boundary. Uint32Array and writeUInt32* postpone the failure to 2106.

Pitfall 3: binary protocols and storage

Code that parses or produces C-defined binary formats in JS (for example buffer.readInt32LE(off) for a time_t field, MessagePack timestamp 32, or a 4-byte field in a custom WebSocket protocol) inherits the format's limit. See File-Formats-and-Protocols. Typical fix:

// BUG
const ts = buf.readInt32LE(8);
// FIX (format change: 8-byte field)
const ts = Number(buf.readBigInt64LE(8));

Node.js fs.stat

Node exposes file times both as Date objects (mtime) and as numbers (mtimeMs, a Number in milliseconds). With { bigint: true }, the numbers become BigInts and mtimeNs (nanoseconds) is added [4]:

const fs = require('fs');
fs.utimesSync('x', 2 ** 31 + 10, 2 ** 31 + 10);
fs.statSync('x', { bigint: true }).mtimeNs;   // 2147483658000000000n
fs.statSync('x').mtime.toISOString();         // "2038-01-19T03:14:18.000Z"

(Tested on 64-bit Linux, 2026-10-21.) Node's JavaScript layer is 2038-safe. What a 32-bit host returns depends on how Node and libuv were compiled. A Node build for armv7 linked against 32-bit time_t gets wrong or failing stat/utimes results for post-2038 file times, the same issue the C page describes. See Lang-C-and-CPP and OS-Linux.

Temporal

The TC39 Temporal API's Temporal.Instant stores epochNanoseconds as a BigInt, limited to the same ±10⁸ days as Date. Values beyond that throw RangeError [5][6]. It removes float-precision concerns and the temptation to use bitwise tricks. Browser support was still incomplete when we last checked, so treat it as optional (availability varies; check MDN compatibility tables).

Server-side and other runtimes

Affected versions / products

Item Representation Limit
Date, Date.now() double ms ±8.64e15 ms (year 275760)
x \| 0, ~~x, x >> 0 signed int32 2038-01-19 03:14:07 UTC (for seconds)
x >>> 0, Uint32Array unsigned int32 2106-02-07 06:28:15 UTC
Int32Array, DataView.setInt32, Buffer.writeInt32* signed int32 2038
Node fs.Stats mtimeMs / mtimeNs double / BigInt Safe in JS. Native layer depends on build
Temporal.Instant BigInt ns ±10⁸ days

Risk & compliance notes

Detection & mitigation

  1. Search: grep -RnE '(Date\.now\(\)|getTime\(\)|/ ?1000)[^;]*(\| ?0|>> ?0|~~)' src/. Also search for Int32Array, writeInt32, readInt32 and setInt32.
  2. ESLint no-bitwise (core rule) flags all bitwise operators. Enable it, or add an allow-list for legitimate uses.
  3. Replace | 0 with Math.trunc() or Math.floor(). Replace 32-bit typed arrays for times with Float64Array or BigInt64Array.
  4. Fake the clock in tests (Jest/Vitest vi.setSystemTime('2038-01-19T03:14:08Z'), Sinon fake timers).
  5. See Testing-and-Detection and Remediation-Guide.

See also

References

  1. Date — JavaScript, MDN Web Docs — epoch, ±8.64e15 ms range (271821 BC to 275760 AD), Invalid Date beyond.
  2. Bitwise OR (|) — JavaScript, MDN Web Docs — operands converted to 32-bit integers; | 0 warning; use Math.trunc().
  3. Int32Array — JavaScript, MDN Web Docs — array of 32-bit signed integers.
  4. File system: fs.Stats / Stat time values — Node.js documentation — mtimeMs number or bigint; mtimeNs bigint only with bigint: true (added v12.10.0).
  5. Temporal.Instant.prototype.epochNanoseconds — MDN Web Docs — BigInt nanoseconds; range ±10⁸ days; RangeError beyond.
  6. Temporal.Instant — TC39 proposal-temporal documentation — Instant stores nanoseconds since the epoch as a bigint.
  7. APAR JR62495: TWDate setTime and getTime JavaScript APIs cause an integer overflow — IBM Support — IBM Business Automation Workflow 20.0.0.1.