Docly Developers
Knowledge base Bug Resolved

Dates are saved as UTC and shown in local time

In shortChanged in production on 23 October 2026: HashJS writes every date as toISOString - UTC, ending in Z - in JSON.stringify, saveJson, saveFile and API responses. The JavaScript getters, toLocaleDateString, docly.format and #value:format# show a date in local time, as JavaScript does, and new Date(year, month, ...) reads its parts as local time. Code that cuts a date out of its text now shows the UTC clock - two hours off in summer, one in winter, and the wrong day just after midnight. Parse the date and format it instead.
Applies to JavaScriptAPIHash templates
  1. What you'll see

    Times on a page are two hours early (one hour in winter), and a date is one day behind for anything that happened between midnight and 02:00. Nothing throws, and older records look right while new ones do not. The code takes the date apart as text:

    let time = o.LastModified.split("T")[1];            // "18:14:57.000Z" - UTC, not local
    let day  = record.Sent.substring(0, 10);             // the UTC date
    let when = ("" + e.time).replace("T", " ").substring(0, 16);

    Or a date built from its parts is a day early once it has been through toISOString:

    new Date(2026, 9, 1).toISOString().slice(0, 10);     // "2026-09-30" - midnight local is 22:00 UTC the day before

    Measured on the server, local time +02:00:

    ExpressionBefore 23 October 2026Now (= JavaScript)
    JSON.stringify(new Date()), saved and API dates"2026-10-23T20:14:57.0643134+02:00""2026-10-23T18:14:57.064Z"
    new Date("2026-10-23T18:14:57Z").getHours()1820
    docly.format("2026-10-23T18:14:57Z", "HH:mm"), #value:HH:mm#18:1420:14
    new Date(2026, 9, 23, 20, 14).toISOString()"2026-10-23T20:14:00.000Z""2026-10-23T18:14:00.000Z"
    isNaN(new Date())truefalse
  2. What's actually happening

    The text of a date is UTC now. JavaScript writes a date as toISOString: UTC, three decimals, a Z at the end. HashJS used to write its own format in local time with an offset. Since 23 October 2026 it writes the JavaScript format everywhere a date leaves the runtime as text — JSON.stringify, docly.saveJson, docly.saveFile, API responses, docly.http* and the console — so a date written in one place reads the same in another. The browser sees it too: a document's LastModified in an API response ends in Z.

    Code that takes the text apart — split("T"), substring(0, 10), substring(11, 16) — never looked at the zone. It only worked because the text used to be local time. Now it reads the UTC clock, and the UTC date, which is yesterday from midnight to 02:00 local time.

    Old files are not rewritten. A record saved before 23 October still holds +02:00 and still looks right, so the fault shows up on new records first. Both forms parse correctly with new Date(...).

    Showing a date uses local time, as in JavaScript. The getters (getHours, getDate, getDay, getMonth, getFullYear …), toLocaleDateString, docly.format and #value:format# convert to the server's local time. Before, they used the zone the date was parsed with, so a date read back from Z showed the UTC clock. That includes dates with a foreign offset: "09:28-05:00" is shown in local time, not as 09:28.

    new Date(year, month, …) reads its parts as local time, as JavaScript does. It used to build a UTC date from them, which looked right in getHours but was two hours ahead in toISOString — and so in every browser that read the JSON.

    Unchanged: toISOString, the getUTC* methods and getTime, strings without a zone (read as local time), and the CLR members .Year, .Month, .Day, .Hour, which keep the zone the date was parsed with — see Date objects carry both CLR and JavaScript members.

  3. What to do

    1. Never cut a date out of its text. Parse it, then format it. On the server:

    docly.format(new Date(o.LastModified), "yyyy-MM-dd HH:mm");   // local time, old and new records alike

    In the browser, let the browser do it:

    new Date(o.LastModified).toLocaleString("nb-NO", { dateStyle: "short", timeStyle: "short" });

    2. Keep text that has no zone as it is. A value your own code wrote with docly.format(new Date(), "yyyy-MM-dd HH:mm") is already local time. If one field can hold both kinds, convert only text that ends in Z or an offset:

    function localIso(v) {
        const s = "" + (v ?? "");
        if (!/T\d\d:\d\d.*(Z|[+-]\d\d:?\d\d)$/.test(s)) return s;   // no zone: already local
        const d = new Date(s);
        if (isNaN(d)) return s;
        const p = n => String(n).padStart(2, "0");
        return d.getFullYear() + "-" + p(d.getMonth() + 1) + "-" + p(d.getDate()) + "T" +
               p(d.getHours()) + ":" + p(d.getMinutes()) + ":" + p(d.getSeconds());
    }
    
    let time = localIso(o.LastModified).split("T")[1];   // the old code, now in local time

    3. A date string from a date you built: use docly.format, not toISOString. toISOString is UTC by definition.

    docly.format(new Date(2026, 9, 1), "yyyy-MM-dd");     // "2026-10-01"
    new Date(2026, 9, 1).toISOString().slice(0, 10);     // "2026-09-30" - wrong

    4. Compare dates as numbers, not as text. "2026-10-23T18:14Z" < "2026-10-23T19:00+02:00" compares characters, and old and new records sort differently. Use new Date(v).getTime().

    5. Guard the input. new Date("") throws in HashJS, and new Date(null) is the current time — see Date.parse and Date.UTC return an object not milliseconds for a small wrapper.

    6. Look for the patterns in your own code: split("T"), substring(0, 10), substring(0, 16), substring(11, slice(0, 10), slice(0, 16) on anything that holds a date, and .toISOString().slice(0, 10) on a date built from its parts. new Date().toISOString().split("T")[0] was UTC before too, and is not affected.

    7. Verify on the instance you run on — one throwaway page with new Date("2026-10-23T18:14:57Z").getHours() settles it. See Use a scratch hash file to test Docly functions.

Last reviewed 23 Oct 2026