Skip to content

Date.prototype.setHours/setDate/etc. throw '(number).setHours is not a function' at runtime #1187

@proggeramlug

Description

@proggeramlug

Summary

Date.prototype mutator methods (setHours, setDate, setMinutes, etc.)
throw TypeError: (number).setHours is not a function at runtime under
perry compile. The Date object created by new Date() doesn't carry the
mutator methods; only the format/getter methods (.toISOString(),
.getHours()) work.

Repro

// repro.ts
export function getExpiry(): Date {
  const expiry = new Date();
  expiry.setHours(expiry.getHours() + 1);  // throws
  return expiry;
}

console.log(getExpiry().toISOString());

Compile + run under perry → fastify handler:

TypeError: (number).setHours is not a function

Note the error spelling — Perry seems to box new Date() as a number
internally; .setHours resolves on the number, not on a Date prototype.

Workaround we shipped

Replace mutators with Date.now() + millis arithmetic, which works fine:

export function getExpiry(): Date {
  return new Date(Date.now() + 60 * 60 * 1000);  // works
}

This pattern is mentioned approvingly in PERRY_GAPS notes (new Date()
cloning via Date.now() arithmetic was a previous fix in our codebase),
so it seems to be the supported path. But it'd be nice if the mutators
either worked or threw a clearer "not implemented" error at compile-time
or first call so the bug doesn't only surface when the route fires in
prod.

Affected methods (untested but presumed all broken)

setHours, setDate, setMinutes, setSeconds, setMilliseconds,
setFullYear, setMonth, setTime, plus the UTC variants.

Context

Found while debugging /api/auth/forgot-password in a Fastify app
compiled with perry. The handler called a getPasswordResetExpiry()
helper that used setHours; the resulting (number).setHours is not a function was caught by the route's try/catch and surfaced as a generic
500 to the client. Took some log-spelunking to track down because the
error message reads like a Perry-internal type tag leak rather than "this
method isn't implemented."

Two adjacent symptoms (not asking you to fix here, just flagging)

While debugging the above I also hit:

  1. request.json() returns undefined under Perry. Stock Fastify
    doesn't have .json() either (it's a Fetch API method, not Fastify),
    so this may be intentional. But the failure mode — undefined return
    instead of a clear error — surprised us; the codebase had ~27 call
    sites that all silently 400'd. request.body works correctly.

  2. return reply.status(400).send({ error: "..." }) serializes to
    literal body null (4 bytes), not the JSON object. Returning the
    object directly (reply.code(400); return { error: "..." };) or
    using implicit serialization (return { ok: true }) both work
    correctly. The reply.status(N).send(obj) chain seems to drop the
    payload somewhere in the Perry Fastify shim.

Happy to file either of these as separate issues if you want them
tracked. Both are reproducible against perry @ main (worker built
2026-05-20).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions