Date parse and UTC return an object not milliseconds
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:
| Call | ECMAScript | HashJS today |
|---|---|---|
Date.parse("2026-11-22T13:44:17") | number (ms) | date object, local time |
Date.parse("2026-11-22T13:44:17Z") | 1795355057000 | null |
Date.parse("2026-11-22T13:44:17.146Z") | number (ms) | null |
Date.UTC(2026, 10, 22, 13, 44, 17) | 1795355057000 | date object |
new Date("2026-11-22T13:44:17Z").getTime() | 1795355057000 | 1795355057000 — 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.parsereturns milliseconds as a number, andNaNfor input it cannot parse or no argument.- Strings with
Z, an offset or milliseconds are parsed, soDate.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.UTCreturns milliseconds;Date.UTC()isNaN, andDate.UTC(2020)is 1 January 2020.cdateis 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.