Skip to content

Polyfill: Validate result in UTC fast path in GetPossibleEpochNanoseconds - #367

Closed
craig-o-curtis wants to merge 1 commit into
js-temporal:mainfrom
craig-o-curtis:port-3205-validate-utc-fast-path
Closed

craig-o-curtis wants to merge 1 commit into
js-temporal:mainfrom
craig-o-curtis:port-3205-validate-utc-fast-path

Conversation

@craig-o-curtis

@craig-o-curtis craig-o-curtis commented Sep 18, 2026

Copy link
Copy Markdown

Ports one tc39/proposal-temporal commit:

What was wrong

GetPossibleEpochNanoseconds has a fast path for the UTC time zone, and that path skipped the IsValidEpochNanoseconds check the other paths make. So near the end of the supported range, a calculation in UTC that needs a date-time after the maximum Instant (+275760-09-13T00:00:00Z) returned a result instead of throwing a RangeError. On main, each of these returns a value:

const MAX = 8640000000000000000000n, D = 86400000000000n, H = 3600000000000n;
const utc = (ns) => new Temporal.ZonedDateTime(ns, 'UTC');

new Temporal.Duration(0).total({ unit: 'days', relativeTo: '+275760-09-12T23:59:60+00:00[UTC]' });
// 0 (the case from tc39/proposal-temporal#3203)
utc(MAX - 3n * D - 5n * H).until(utc(MAX), { largestUnit: 'day', smallestUnit: 'hour' });
// P3DT5H
Temporal.Duration.from('PT49H').total({ unit: 'day', relativeTo: utc(MAX - 2n * D - H) });
// 2.0416666666666665

The same calls with +00:00 or Europe/London in place of UTC already throw a RangeError on main.

What this changes

The UTC fast path now checks its result with ValidateEpochNanoseconds, like the other paths. All three calls above throw RangeError: date/time value is outside of supported range, the same as with +00:00.

Results inside the range don't change. In both UTC and +00:00, these give the same results on main and on this branch:

  • PlainDateTime at the maximum converted to ZonedDateTime, and one nanosecond past it (which throws).
  • until() near the maximum without rounding (P3DT5H).
  • PT23H total in days relative to one day before the maximum (0.9583333333333334).
  • startOfDay() at the maximum.
  • An ordinary rounded until() (P1DT2H).

Not fixed by this PR

This problem has nothing to do with UTC, but it is also near the maximum Instant. It behaves the same on main and on this branch. Within one search window of the maximum Instant, getTimeZoneTransition('next') can miss a transition that exists, and code that depends on it then throws:

Temporal.Instant.from('+275760-09-01T00:00:00Z').toZonedDateTimeISO('America/Santiago').getTimeZoneTransition('next');
// null, but getTimeZoneTransition('previous') from the maximum Instant finds
// +275760-09-07T01:00:00-03:00[America/Santiago]
Temporal.PlainDate.from('+275760-09-07').toZonedDateTime('America/Santiago');
// TypeError: Cannot read properties of null (reading 'sign')

GetNamedTimeZoneNextTransition in proposal-temporal's polyfill has the same code, so I'll report it there first.

Testing

Node 24.21.0 (ICU 78.3, CLDR 48):

  • jest passes (607 tests).
  • test262 at the pinned version passes in both the default and NODE_ENV=production builds: 4828 passed, 0 failed, 0 passed unexpectedly.

The test262 test for this fix, built-ins/Temporal/Duration/prototype/total/relativeto-date-limits.js, is newer than our pinned test262. It got its UTC case in tc39/test262@416f2a3de8, which also postdates proposal-temporal's test262 pin when #3205 merged. I ran it by hand at that commit, along with its sibling Duration/prototype/round/relativeto-date-limits.js:

Build total/relativeto-date-limits.js round/relativeto-date-limits.js
main fails: "+275760-09-12T00:00:01+00:00[UTC] is out of range as a relativeTo argument for total Expected a RangeError to be thrown but no exception was thrown at all" passes
this PR passes passes

Differences from upstream (TypeScript)

None. The change is the same three lines as upstream's.

…onds

The `IsValidEpochNanoseconds` check was previously omitted when the
time zone was UTC.

NOTE: (this info below wasn't in the upstream commit)
Ports tc39/proposal-temporal#3205, which fixed
tc39/proposal-temporal#3203. Near the end of the supported range, a
calculation in UTC that needs a date-time after the maximum Instant
returned a result instead of throwing a RangeError. The "+00:00" and
named time zone paths already threw. For example, this returned 0 and
now throws a RangeError:

  new Temporal.Duration(0).total({
    unit: 'days',
    relativeTo: '+275760-09-12T23:59:60+00:00[UTC]'
  })

UPSTREAM_COMMIT=d90d432b2ff10ffc4149dd0ba8f0aece21150adc
@ptomato

ptomato commented Sep 21, 2026

Copy link
Copy Markdown
Contributor

Thanks for proactively porting this, but I'd prefer to keep the commits ported in order — otherwise the rebases become unmanageable. I apologize that it's been taking a while.

@ptomato ptomato closed this Sep 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Polyfill: UTC fast-path in GetPossibleEpochNanoseconds does not validate the result

3 participants