JavaScript and TypeScript
In brief: JavaScript's
Datecounts 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| 0shortcut,Int32Arraybuffers, 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
- At risk ConfirmedJavaScript bitwise ops (|0, >>0), Int32Array, Buffer.writeInt32 on secondsThese coerce values to signed 32-bit, so epoch seconds wrap in 2038.Do: Use Math.floor or Math.trunc, and Float64Array, BigInt64Array or readBigInt64LE for stored times.
Not affected or different deadline (1), and why
- Not affectedJavaScript Date and Temporal.InstantDate uses double-precision milliseconds, valid to the year 275760.
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
- JSON has no date type. Timestamps travel as numbers, which are safe in JS, but a backend that parses them into
int32brings the bug back. Agree on 64-bit (or ISO 8601 strings) in API schemas. - IBM Business Automation Workflow shipped a JavaScript
TWDate.getTime()/setTime()API returning seconds that overflowed (APAR JR62495, 20.0.0.1) [7]. This is an example of a platform API, not the language, introducing the limit. - Embedded JS engines (Espruino, JerryScript, Duktape, QuickJS on microcontrollers) may be built with 32-bit integers or single-precision floats. Check each engine's configuration (engine-specific; not verified here).
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
- Front-end risk is low but easy to miss: expiry banners, countdowns and token-refresh logic that use
| 0. - Back-end Node risk is mainly at binary and database boundaries.
- IoT gateways running Node on 32-bit Arm need both a 64-bit-time OS build and audited code.
Detection & mitigation
- Search:
grep -RnE '(Date\.now\(\)|getTime\(\)|/ ?1000)[^;]*(\| ?0|>> ?0|~~)' src/. Also search forInt32Array,writeInt32,readInt32andsetInt32. - ESLint
no-bitwise(core rule) flags all bitwise operators. Enable it, or add an allow-list for legitimate uses. - Replace
| 0withMath.trunc()orMath.floor(). Replace 32-bit typed arrays for times withFloat64ArrayorBigInt64Array. - Fake the clock in tests (Jest/Vitest
vi.setSystemTime('2038-01-19T03:14:08Z'), Sinon fake timers). - See Testing-and-Detection and Remediation-Guide.
See also
References
- Date — JavaScript, MDN Web Docs — epoch, ±8.64e15 ms range (271821 BC to 275760 AD), Invalid Date beyond.
- Bitwise OR (|) — JavaScript, MDN Web Docs — operands converted to 32-bit integers;
| 0warning; useMath.trunc(). - Int32Array — JavaScript, MDN Web Docs — array of 32-bit signed integers.
- File system: fs.Stats / Stat time values — Node.js documentation —
mtimeMsnumber or bigint;mtimeNsbigint only withbigint: true(added v12.10.0). - Temporal.Instant.prototype.epochNanoseconds — MDN Web Docs — BigInt nanoseconds; range ±10⁸ days; RangeError beyond.
- Temporal.Instant — TC39 proposal-temporal documentation — Instant stores nanoseconds since the epoch as a bigint.
- APAR JR62495: TWDate setTime and getTime JavaScript APIs cause an integer overflow — IBM Support — IBM Business Automation Workflow 20.0.0.1.