Date parse and UTC return an object not milliseconds

Bug Active
In HashJS, Date.parse and Date.UTC return a date object instead of a number of milliseconds, and Date.parse returns null for ISO 8601 strings that end in Z, carry an offset or carry milliseconds - which includes everything toISOString() produces. Code written to the ECMAScript standard computes the wrong answer without throwing. This is a defect; a platform fix is made and pending deployment. Until it lands, use new Date(value).getTime().
Applies to: JavaScriptAPIHash templates

What you'll see

A date comparison never fires, or fires every time. A cleanup job never deletes anything, a cache is never considered fresh, a link never expires — and nothing throws. The code reads like ordinary JavaScript:

let t = Date.parse(record.created);          // record.created = "2026-08-11T08:52:59.146Z"
if (t && (Date.now() - t) > 7 * DAY) docly.deleteFile(path);   // never runs

let age = Date.now() - Date.parse(cached.updated);                // not a number of ms
if (Date.parse(expires) > Date.now()) { /* ... */ }              // compares an object with a number

Measured in an API endpoint:

CallECMAScriptHashJS today
Date.parse("2026-11-22T13:44:17")number (ms)date object, local time
Date.parse("2026-11-22T13:44:17Z")1795355057000null
Date.parse("2026-11-22T13:44:17.146Z")number (ms)null
Date.UTC(2026, 10, 22, 13, 44, 17)1795355057000date object
new Date("2026-11-22T13:44:17Z").getTime()17953550570001795355057000 — correct

What's actually happening

Date.parse is the template function cdate under another name. It was registered as an alias for cdate, which returns a date object, and which only accepts the two exact patterns yyyy-MM-dd and yyyy-MM-ddTHH:mm:ss. Anything with a zone suffix (Z, +02:00) or with milliseconds does not match, and comes back as null. That rules out exactly the strings JavaScript itself produces: new Date().toISOString() always ends in .sssZ.

Date.UTC builds the right UTC date, but returns the date object rather than its millisecond value.

The failure is silent because both wrong values are falsy or plausible. null makes a guard like t && … skip quietly — so a job that deletes expired records simply never deletes. A date object in arithmetic produces a value no comparison against a millisecond count can meaningfully satisfy. Nothing throws, so nothing is logged.

After the fix (pending deployment):

  • Date.parse returns milliseconds as a number, and NaN for input it cannot parse or no argument.
  • Strings with Z, an offset or milliseconds are parsed, so Date.parse("2026-11-22T13:44:17Z") === 1795355057000.
  • Strings without a zone are read as local server time, as before. That includes a date alone ("2026-11-22") — which ECMAScript reads as UTC. That difference remains.
  • Date.UTC returns milliseconds; Date.UTC() is NaN, and Date.UTC(2020) is 1 January 2020.
  • cdate is unchanged and still returns a date object.

Standard code such as the examples above starts working after the fix without changes. Code that relies on the defect — calling .getTime() on the result of Date.parse, or checking it for null — breaks.

What to do

1. Until the fix is deployed, convert with new Date(value).getTime(). It handles Z, offsets and milliseconds correctly today, and keeps working after the fix:

let t = new Date(record.created).getTime();   // 1786438379146, not null

2. Guard the input first. new Date("") and new Date("garbage") throw in HashJS, while new Date(undefined) and new Date(null) both return the current time rather than an invalid date — so a missing field silently looks like "just now". Wrap it once and use the wrapper:

function toMs(value) {
    if (value === null || value === undefined || value === "") return NaN;
    try { return new Date(value).getTime(); } catch (e) { return NaN; }
}

let t = toMs(record.created);
if (!isNaN(t) && (Date.now() - t) > 7 * DAY) docly.deleteFile(path);

3. Do not build on the defect. Never call a date method on the result of Date.parse or Date.UTC, and never test either for null. Both stop working when the fix lands. Treat the result as a number and test with isNaN.

4. Store the number next to the text. Saving expiresMs: expMs alongside expires: new Date(expMs).toISOString() means comparisons never need to parse at all. It is the sturdiest option, before and after the fix.

5. Say which zone a date-only string is in. If "2026-11-22" means midnight UTC, write "2026-11-22T00:00:00Z". HashJS reads a bare date as local time.

6. Check cleanup and expiry code you already have. A job guarded by t && … has likely never acted on ISO strings with Z. When the fix is deployed it will catch up on the whole backlog in its first run — count what it will delete before that happens.

7. Verify on the instance you run on. One throwaway page shows whether typeof Date.parse("2020-01-01T00:00:00Z") is "number" yet — see Use a scratch hash file to test Docly functions.