Docly Developers
Knowledge base Pitfall By design

Date objects carry both CLR and JavaScript members

In shortA Date in HashJS answers to both the CLR and the JavaScript API on the same instance. The two disagree about the month: .Month is 1-based and .getMonth() is 0-based. .Year and .getFullYear() agree, and .Day and .getDate() agree - which is what makes the month trap easy to miss, because a spot check on the year suggests the two are interchangeable. They also disagree about the zone: the CLR members keep the zone the date was parsed with, while the JavaScript getters and docly.format show local time. For a date read back from JSON - UTC since 23 October 2026 - .Hour and .Day are the UTC clock.
Applies to JavaScriptHash templatesAPI
  1. What you'll see

    A date is off by exactly one month, in some code paths and not others. Filters miss records at a month boundary, a generated period label names the wrong month, or a comparison between two dates built in different parts of the file disagrees with itself.

    Both spellings look correct in isolation, because both are real members of the same object:

    let d = new Date();
    
    d.Year;            // 2026   (CLR)
    d.getFullYear();   // 2026   (JS)     - agree
    
    d.Month;           // 8      (CLR, 1-based)
    d.getMonth();      // 7      (JS,  0-based)   - do NOT agree
    
    d.Day;             // 20     (CLR)
    d.getDate();       // 20     (JS)     - agree

    Or a time is two hours early (one in winter), but only for dates that came back from JSON or an API response. The CLR member reads the zone the date was parsed with:

    let d = new Date("2026-10-23T18:14:57Z");   // as saved since 23 October 2026
    
    d.Hour;            // 18     (CLR, the zone it was parsed with)
    d.getHours();      // 20     (JS,  local time)
  2. What's actually happening

    A Date here is a .NET (CLR) value exposed to HashJS, and it keeps both APIs. The CLR members (.Year, .Month, .Day) and the JavaScript members (.getFullYear(), .getMonth(), .getDate()) sit on the same instance, each carrying its own platform's convention unchanged.

    Those conventions differ in exactly one place. .NET numbers months from 1; JavaScript numbers them from 0. Everything else lines up, and that is the whole difficulty: .Year and .getFullYear() returning the same number is not evidence that the two APIs are interchangeable, but it reads like it. Code then picks whichever spelling came to hand, and a calculation that mixes them is wrong by one month in every month of the year.

    The failure is quiet because both values are plausible. An off-by-one month is not obviously garbage the way a NaN or an empty render is — it is a real month, just the wrong one, and it only looks wrong to someone who knows what the answer should have been.

    The zone is the second difference. The CLR members are the parts of the value as it was parsed: a date read from "…Z" has UTC parts. The JavaScript getters, toLocaleDateString, docly.format and #value:format# convert to local time first, as JavaScript does. For new Date() and for text without a zone the two agree, because both are already local — which is why the difference only shows up on dates that came back from JSON. Since 23 October 2026 that is every date HashJS saves; see Dates are saved as UTC and shown in local time.

  3. What to do

    1. Pick one API per expression and stay inside it. Mixing the two in a single calculation is off by one, always:

    // consistent - CLR throughout
    let label = d.Year + "-" + d.Month;
    
    // consistent - JavaScript throughout
    let label = d.getFullYear() + "-" + (d.getMonth() + 1);
    
    // wrong - mixes the two
    let label = d.Year + "-" + d.getMonth();

    2. Use the JavaScript getters or docly.format for anything a person will read. They show local time. The CLR members (.Hour, .Day, .Month) are the parts in the zone the date was parsed with — UTC for any date read back from JSON — so .Day is yesterday's date for anything saved between midnight and 02:00. Use a CLR member only on a date you created locally with new Date(), and write getMonth() + 1 rather than mixing in .Month.

    3. Reviewing code you did not write, check the month spellings first when a date is off by one. The bug is almost never in the arithmetic around it.

    4. For formatting, do not assemble the string yourself. docly.format handles month names and ordering, and respects the culture — but a request starts with the invariant culture, so set one first. See A request starts with the invariant culture.

    5. When in doubt, print both. One throwaway page settles which member you are actually holding — see Use a scratch hash file to test Docly functions.

Last reviewed 23 Oct 2026