Clamp a Date to a Min/Max Range in JavaScript

To clamp a date into [min, max], compare it to each bound and return the bound it passes — a type-safe alternative to Math.min/Math.max. Part of Working with Date Ranges and Intervals.

Why this scenario is tricky

Clamping — pinning a value into a [min, max] window — is a three-line function, and the trap is that the obvious three lines are Math.min(Math.max(value, min), max). Math only understands numbers, so it silently coerces each argument through valueOf(). For a Date that coercion happens to work, because a Date is a number of milliseconds underneath, so the bug hides. Swap in Temporal values and the same code throws, because Temporal types deliberately refuse to coerce to a number — a design choice that stops you from accidentally doing arithmetic on a date. The lesson is that clamping should be expressed with explicit comparisons, not numeric helpers, so the logic reads the same and behaves the same for Date, Instant, and PlainDate alike.

Clamping a date into [min, max] looks like Math.min(Math.max(...)), but Math only works on numbers — pass it Date objects and it coerces them through valueOf(), which happens to work for Date but silently fails for Temporal values, which throw rather than coerce. The robust pattern is to clamp with explicit compare calls so the logic is identical for instants and civil dates, and so an inverted [min, max] is caught instead of producing a nonsense result.

Clamp with compare, not Math.min/maxMath coerces Dates and throws on TemporalClamp with compare, not Math.min/maxMath.min(Math.max(d,min),max)throws on Temporal valuescompare-based clampworks for any comparableExplicit comparison keeps clamping correct for both Date and Temporal types.

Minimal working solution

Two comparisons pick the bound.

The comparison form says exactly what clamping means: if the value is below the floor, return the floor; if it is above the ceiling, return the ceiling; otherwise return it unchanged. Written with Temporal.PlainDate.compare, each branch is a single call whose sign is unambiguous, and because the function never touches valueOf() it works for any comparable Temporal type. It also has a property Math.min/Math.max lacks: it returns the original value when it is already in range, rather than a fresh object, which can matter if callers rely on identity.

import { Temporal } from '@js-temporal/polyfill';
const cmp = Temporal.PlainDate.compare;
function clamp(d, min, max: Temporal.PlainDate) {
  if (cmp(d, min) < 0) return min; // below the floor
  if (cmp(d, max) > 0) return max; // above the ceiling
  return d;
}

Clamp into [min, max]Return the nearest bound when outsideClamp into [min, max]d, min, maxd<min? mind>max? maxelse d

Full production version

Validate the bounds and keep the function generic over any Temporal type via its static compare.

A production clamp should guard its own preconditions. If min is after max the window is empty and no input can satisfy it, so the honest response is to throw rather than return an arbitrary bound — an inverted window is almost always a caller bug, and surfacing it immediately beats returning a plausible-looking wrong date. Making the function generic over the comparator (compare: (a, b) => number) lets one implementation clamp instants, civil dates, and zoned datetimes, since each Temporal type ships a static compare with the same shape. Half-bounded windows — a minimum with no maximum, or vice versa — fall out naturally if you let a null bound mean 'no limit on that side' and skip the corresponding comparison.

import { Temporal } from '@js-temporal/polyfill';
function clampDate<T extends { }>(d: T, min: T, max: T, compare: (a: T, b: T) => number): T {
  if (compare(min, max) > 0) throw new RangeError('min after max');
  if (compare(d, min) < 0) return min;
  if (compare(d, max) > 0) return max;
  return d;
}
// clampDate(d, min, max, Temporal.Instant.compare)

A generic clampPass the type's compare; reject inverted boundsA generic clampd,min,maxmin>max?throwcompare dclamped

Verification snippet

Three regions need coverage: below the floor (returns min), inside the window (returns the value unchanged), and above the ceiling (returns max). Add the two boundary cases — a value exactly equal to min and one exactly equal to max — and assert they are returned as-is, since an off-by-one in the comparison operator shows up precisely there. Finally, assert that an inverted window where min is after max throws, so the precondition guard is exercised rather than silently trusted.

Clamp assertionsBelow, inside, and above assert correctlyAssertions that prove the edge cased < minreturns minmin <= d <= maxreturns dd > maxreturns maxmin > maxthrows

Common pitfalls

The headline pitfall is Math.min/Math.max on date values: it works by accident for Date and throws for Temporal, so code that passes tests with one type breaks with the other. The second is skipping the inverted-bounds check, which lets a min > max window return a confidently wrong bound. The third is clamping values of mixed types — a Date against PlainDate bounds — where the coercion drags in a time-of-day and tips the comparison. Keep the value and both bounds in one comparable type, validate the window, and clamp with explicit compare calls.

Clamping shows up wherever a date is constrained to a window. A date picker offers min and max props and must snap or reject an out-of-range selection; clamping the parsed value keeps the field honest even when a user types a date directly. A booking flow constrains check-in to no earlier than today and no later than the bookable horizon. A report defaults its date filter to the last full quarter but clamps any user override to the range for which data exists. A subscription proration clamps an event date into the current billing period before computing charges. In each case the clamp is the guard that stops downstream code from ever seeing a date outside the window, which is far cheaper than scattering range checks through every consumer.

Clamping a single date generalises naturally to clamping a whole range into a window: clamp the start up to the window's floor and the end down to its ceiling, and if the clamped start is no longer before the clamped end, the range fell entirely outside the window and the result is empty. That operation is exactly the intersection of the range with the window, which is why the same comparison primitives underpin both this page and the overlap logic in the parent guide. Seeing clamping and intersection as two faces of the same endpoint arithmetic is what lets a small set of compare-based helpers cover most of an application's date-window needs.

Clamp pitfallsNumeric coercion and inverted boundsWrongRightMath.min/max on datesthrows or coercescompare-based clamptype-safeignore min>maxsilent wrong boundvalidate bounds firstfail loud

There is a subtle behavioural property worth calling out: a good clamp returns the original value unchanged when it is already inside the window, not a freshly constructed equal value. For immutable Temporal types this rarely matters, but it makes the function a true no-op in the common case and avoids surprising identity comparisons. The mirror property is that clamping is idempotent — clamping an already-clamped value changes nothing — which is a useful invariant to assert in tests. If you ever need to clamp into a window whose bounds are themselves civil dates but whose value is an instant, convert the value into the same calendar frame first rather than comparing across types, so the comparison stays meaningful.

A last practical note concerns reactive UIs. In a framework that re-renders on state change, clamping on the way into state — rather than on the way out — means every consumer of that state sees an already-valid date and no component has to defend against an out-of-range value. Pair the clamp with the inverted-bounds guard so a misconfigured min/max surfaces as an error at the edge of the system rather than as a silently pinned date deep in a chart. The combination — validate the window once, clamp into state once — replaces a scattering of defensive checks with a single choke point, which is the same design discipline that makes the half-open interval and the compare-based predicates pay off across the rest of this topic.

It helps to contrast clamping with its close cousins so you reach for the right one. Clamping pins an out-of-range value to the nearest bound, which is what you want when any value is acceptable but must be corralled — a slider, a constrained date field. Rejecting an out-of-range value throws or returns an error instead, which is what you want when an out-of-range input signals a real mistake that should not be silently corrected. Wrapping maps the value back into the window modulo its length, which suits cyclic quantities like a time-of-day but almost never a calendar date. Choosing clamp when you meant reject is a classic source of silent data corruption: the user typed a date outside the allowed window, the clamp quietly moved it to the boundary, and no one noticed until a report looked wrong. Name the behaviour you intend, and make the function do exactly that and nothing more.

Bringing it together, a robust date clamp is a handful of explicit comparisons wrapped around a validated window, generic over the Temporal type through its static compare, tolerant of open-ended bounds via null, and honest about inverted input by throwing. That small function then becomes a dependable building block: date pickers, booking windows, report filters, and proration all lean on the same guarantee that a value handed downstream is inside the window. The recurring theme across date-range work — half-open intervals, one comparable type, explicit comparisons, validation at the boundary — is exactly what makes clamping trivial to get right once you stop reaching for Math and start comparing dates as dates.

In short, prefer an explicit compare-based clamp over Math.min/Math.max, validate that the minimum is not after the maximum, allow null for an open side, and decide deliberately whether an out-of-range value should be pinned or rejected. Those four habits turn a deceptively simple three-line function into one you can rely on across every date type and every window in the codebase.

Frequently Asked Questions

How do I clamp a date between a minimum and maximum?

Compare the date to each bound and return the bound it exceeds: if it is before min return min, if after max return max, otherwise return it unchanged. Using Temporal.compare instead of Math.min/max keeps the logic working for both instants and civil PlainDate values.

Why not use Math.min and Math.max on dates?

Math operates on numbers, so it coerces Date via valueOf() — which works by accident — but throws on Temporal values, which do not coerce to numbers. A compare-based clamp is explicit, type-safe, and reads the same for Date, Instant, and PlainDate.

Can I clamp a date with Math.min and Math.max?

You can for a legacy Date, because it coerces to a number, but it is fragile and it throws for Temporal values, which refuse numeric coercion. A compare-based clamp — return min if the value is below it, max if above, otherwise the value — is type-safe across Date, Instant, and PlainDate and reads the same in every case.

How do I clamp with only a minimum or only a maximum?

Let a null bound mean no limit on that side and skip the corresponding comparison. With a null maximum the function only enforces the floor; with a null minimum it only enforces the ceiling. That keeps one implementation serving fully bounded, half-bounded, and unbounded windows without special-case functions.