RangeError: Invalid Time Value — Every Cause and How to Fix It

Published 20 Sep 2026 · 8 min read

RangeError: Invalid time value is one of the more disorienting JavaScript errors, because the line it points at is rarely the line that actually did anything wrong. new Date('garbage') doesn't throw — it silently returns an "Invalid Date" object. The crash only happens later, the moment something calls toISOString() on that object — the one Date method that actually throws on it; toJSON() (what JSON.stringify() calls internally) just returns null, and toUTCString()/toString() just return the literal text "Invalid Date" — which by then can be several function calls away from wherever the bad value actually originated. This walkthrough covers five real causes, each verified directly in Node, plus a live example in Time Machine for the ones its epoch converter can actually reproduce.

Found this while writing this piece: Time Machine's own epoch-to-date converter had this exact bug. Pasting a timestamp outside JavaScript's representable date range crashed the whole tool with a blank page, because it called .toISOString() on an Invalid Date without checking first — cause #3 below, happening on our own site. Fixed the same day this was published; see the note at the bottom.

Quick reference

SymptomMost likely causeFix
Crash traces back to a date parsed from user or API textUnparseable date stringCheck isNaN(date.getTime()) right after parsing, before using the value for anything
Crash only on some records, tied to one fieldnew Date(undefined) from a missing/renamed API fieldValidate the field exists before constructing a Date from it
Crash only on specific numeric timestampsEpoch value outside the representable range (often a nanosecond or microsecond value fed in as milliseconds)Confirm the source's actual unit (s / ms / μs / ns) before constructing the Date
Crash right after a form is submitted with a field left blanknew Date('') from an empty form fieldGuard against empty strings before parsing, same as any other missing input
Crash happens on a date that was valid earlier in the functionNaN arithmetic silently invalidating an in-place Date mutationValidate the operand before calling setDate()/setTime()/etc., not just the result

1. An unparseable date string

The most common real-world trigger: a date string arrives from user input, a CSV import, or a third-party API in a format the Date constructor can't parse — and different engines disagree on what counts as parseable at all, which is exactly how this slips past testing on one browser and crashes in production on another.

const date = new Date('13/45/2026'); // not a real date, and not ISO 8601 either
console.log(date); // Invalid Date — no error yet

JSON.stringify({ createdAt: date });
// {"createdAt":null} — no crash. Date.prototype.toJSON() quietly returns
// null for an Invalid Date, so JSON.stringify() never throws on its own.

const payload = { createdAt: date.toISOString() }; // building the response by hand instead
// Uncaught RangeError: Invalid time value
// toISOString() is the one Date method that actually throws — this is the
// real, common trigger: code that formats the date explicitly before
// handing it to JSON.stringify(), rather than letting toJSON() run.

The fix is the same one-line check every time, run immediately after parsing rather than wherever the crash happens to surface:

const date = new Date(rawInput);
if (isNaN(date.getTime())) {
  throw new Error(`Unparseable date: "${rawInput}"`); // fail loud, at the source
}

2. A missing or renamed API field

new Date(undefined) is Invalid Date — but new Date(null) is not; it's a valid date at the epoch (1 January 1970). That asymmetry is a real trap: a destructured field that's simply missing from an API response (renamed in a backend refactor, absent for some record types, or optional-and-actually-absent) produces undefined, not null, and silently becomes an Invalid Date rather than a loud, obvious failure at the point of destructuring.

const { createdAt } = apiResponse; // field was renamed to created_at server-side
const created = new Date(createdAt); // createdAt is undefined
// ...twelve lines later, in a completely different function...
created.toISOString();
// Uncaught RangeError: Invalid time value

Validate the field exists at the boundary where the response is received, not wherever it happens to get used — that's what actually shortens the distance between the bug and its symptom.

3. An epoch value outside the representable range

JavaScript's Date can only represent roughly ±8,640,000,000,000,000 milliseconds from the epoch — about ±273,790 years. No realistic calendar date exceeds that on its own; the practical trigger is a units mismatch. Go's time.UnixNano(), some structured loggers, and a handful of databases emit epoch time in nanoseconds rather than milliseconds. Feed a nanosecond value straight into new Date(), which expects milliseconds, and you overshoot the representable range by three orders of magnitude.

const nanoTimestamp = 1789905600000000000; // a real epoch-nanoseconds value
const date = new Date(nanoTimestamp); // Date expects milliseconds, not nanoseconds
console.log(date); // Invalid Date
date.toISOString();
// Uncaught RangeError: Invalid time value

See it fail safely, live

Paste that exact nanosecond value into Time Machine's epoch field and it now reports "Invalid (out of representable range)" instead of crashing the page — the isNaN(date.getTime()) check from the fix above, added the same day this piece was written:

Open this example in Time Machine

Microsecond values are the trap inside the trap: a microsecond epoch for right now is about 1.79e15 (16 digits) — still inside the ±8,640,000,000,000,000ms range, so it doesn't overshoot and doesn't throw. Fed straight into new Date() as if it were milliseconds, it silently produces a wrong-but-valid date around the year 58,000 instead of crashing — the same silent-corruption failure mode as the seconds-vs-milliseconds mix-up below, just a units error one step further out. Only nanoseconds (19 digits) are actually large enough to exceed the representable range and throw.

A milder version of the same units confusion — a seconds value misread as milliseconds — also doesn't exceed the representable range, so it never throws either; it just lands on a wrong-but-valid date decades off instead of centuries. Time Machine's own epoch field actually catches this specific case automatically, by checking the pasted value's digit length before deciding whether to treat it as seconds or milliseconds:

See the auto-detection, live

1789905600 is a real "seconds" epoch value for this month. Paste it in raw and the tool still lands on the right 2026 date, because 10 digits reads as seconds — code that skips that check and always assumes one unit won't be so lucky:

Open this example in Time Machine

4. An empty string from an unfilled form field

new Date('') is Invalid Date, same as any other unparseable string — but it's worth calling out on its own because it's the one cause that's entirely about UI state rather than data format. A date picker that allows clearing its value, a controlled input that starts blank before the user has typed anything, or a form re-render between "cleared" and "submitted" can all hand an empty string straight to new Date().

function scheduleReminder(dateStr) {
  const when = new Date(dateStr); // dateStr is '' — field was cleared, not filled
  return when.toISOString(); // Uncaught RangeError: Invalid time value
}

See the safe version, live

Time Machine's own "Date to Epoch" panel uses a native <input type="datetime-local"> picker rather than free-text parsing — clear it in Time Machine and nothing crashes, because the handler checks isNaN() before ever touching the result. That's the same one-line guard from the reference table above, just applied at the UI layer instead of an API boundary.

5. NaN arithmetic silently invalidating a valid Date

The sneakiest of the five, because the Date object is genuinely valid right up until the mutation that breaks it — and Date's in-place setters (setDate(), setTime(), setMonth(), etc.) never throw on a bad operand either. They just quietly leave the object holding NaN, ready to explode wherever it gets formatted next.

const dueDate = new Date('2026-09-20'); // valid so far
const extensionDays = parseInt(req.query.extend); // '' or missing → NaN, not an error
dueDate.setDate(dueDate.getDate() + extensionDays); // NaN silently invalidates the Date in place
// ...later, in a response serializer...
res.json({ dueDate: dueDate.toISOString() }); // Uncaught RangeError: Invalid time value
// res.json({ dueDate }) alone wouldn't throw — Express's JSON.stringify()
// would call toJSON() and silently send { "dueDate": null }. The crash
// needs an explicit toISOString() somewhere in the response-building code,
// same as cause 1 — but that's a common pattern for exactly this "always
// send ISO 8601 strings, don't rely on the default" reason.

Validate the operand, not just the eventual result — by the time toISOString() fails, the original bad value (NaN from a missing query param, in this example) is long gone from the stack trace:

if (isNaN(extensionDays)) {
  throw new Error('extend must be a number');
}
dueDate.setDate(dueDate.getDate() + extensionDays);

Debugging checklist

  1. The error's own stack trace points at the toISOString() call — not the cause. Work backward from there to find where the Date was constructed or last mutated. (toJSON() and toUTCString() look like the same kind of call but don't throw — see the FAQ above — so if the stack trace names one of those instead, this isn't that bug.)
  2. Check isNaN(date.getTime()) immediately after construction. If it's already NaN, the problem is in the input value, not later code — jump to causes 1–4.
  3. If the Date was valid earlier and only breaks after a set*() call, the problem is NaN arithmetic (cause 5) — check whatever value was passed into that setter.
  4. If the input is a raw number, count its digits before assuming a unit: 10 is almost always seconds, 13 is milliseconds, 16 is microseconds, 19 is nanoseconds. Only 19-digit (nanosecond) values fed straight into new Date() actually overshoot the representable range and throw — a 16-digit microsecond value stays in range and produces a silently wrong date instead, so "no crash" doesn't mean "no units bug."
  5. Remember the null vs. undefined asymmetry: new Date(null) is a silently "valid" epoch-zero date, not an error — if a date is defaulting to 1 January 1970 instead of crashing, that's this same missing-field bug wearing a different disguise.

Two related debugging pieces worth knowing about: Unexpected Token in JSON covers what happens when a date (or anything else) fails to parse as JSON in the first place, and the Unix timestamp guide covers epoch time from the ground up if the seconds/milliseconds/microseconds/nanoseconds distinction above is new territory.

The fix shipped with this piece: Time Machine's epoch converter called .toISOString() on whatever Date it built from your pasted input, with no check for whether that Date was actually valid — the exact bug this article is about, live on our own tool. An out-of-range paste (like the nanosecond example above) crashed the page with no fallback UI. It now checks isNaN(date.getTime()) first and reports "Invalid (out of representable range)" instead of crashing, the same fix pattern recommended in every cause above.

🕐 Paste a timestamp and see it handled safely

Time Machine converts Unix epoch time to and from human-readable dates entirely in your browser — including out-of-range and malformed input, which it now reports instead of crashing on.

Open Time Machine →

← Back to Blog · See also: Unexpected End of JSON Input →