Get All Dates Between Two Dates in JavaScript

To list every day between two dates, iterate a Temporal.PlainDate with .add({ days: 1 }) until you reach the end β€” calendar-day steps that never drift across DST. Part of Working with Date Ranges and Intervals.

Why this scenario is tricky

Enumerating the days in a range sounds like a for loop with a counter, and for pure calendar work it very nearly is β€” the subtlety is entirely in what you step. If you step by adding 24 hours to a Date, the loop is anchored to absolute time, and on the one day a year that daylight saving removes an hour, 24 hours of absolute time carries you from midnight to 01:00 the next day rather than to the next midnight. Do that in a long enough loop and the accumulated drift eventually skips a calendar day outright, or lands two iterations on the same date. The bug is invisible in testing because it only appears for ranges that straddle a transition in a zone that observes DST, which is exactly the kind of input a test suite pinned to UTC never generates.

Enumerating the days between two dates by adding 24 hours in a loop drifts across a DST transition: on the spring-forward day, 24 hours lands you at 01:00 the next day instead of 00:00, and after enough iterations you skip or repeat a date. The fix is to iterate on a calendar type β€” Temporal.PlainDate β€” where .add({ days: 1 }) means the next calendar day regardless of how many real hours it contained.

Iterate calendar days, not 24-hour stepsAdding 24 hours drifts across DSTIterate calendar days, not 24-hour stepscursor += 86_400_000 msskips/repeats a day at DSTcursor.add({ days: 1 })always the next calendar dayPlainDate iteration is immune to the 23- and 25-hour days DST creates.

Minimal working solution

Loop while the cursor is before the end, collecting each PlainDate.

Stepping a Temporal.PlainDate sidesteps the problem completely, because a PlainDate has no time and no zone β€” it is a pure calendar date, and .add({ days: 1 }) always advances to the next calendar day whether that real day was 23, 24, or 25 hours long. The loop condition uses Temporal.PlainDate.compare(cursor, end) < 0, which is the half-open convention written literally: the start is included and the end is excluded, so a range from Monday to Thursday yields Monday, Tuesday, and Wednesday. That exclusivity is what lets you enumerate two adjacent ranges back to back without the shared boundary date appearing in both lists.

import { Temporal } from '@js-temporal/polyfill';
function datesBetween(start: Temporal.PlainDate, end: Temporal.PlainDate) {
  const out: Temporal.PlainDate[] = [];
  // Half-open: include start, exclude end.
  for (let d = start; Temporal.PlainDate.compare(d, end) < 0; d = d.add({ days: 1 })) out.push(d);
  return out;
}

Walk the calendarAdd one calendar day until you reach the endWalk the calendarcursor=startpush, add{days:1}stop at end

Full production version

A generator avoids allocating the whole array for long ranges, and a step parameter supports every-N-days.

For most UI work β€” rendering a month grid, listing the days of a booking β€” a plain array is fine, and materialising it up front is the simplest thing that works. But a generator earns its keep the moment the range is open-ended or very long: a five-year audit window is over 1,800 dates, and if you only need to find the first date matching some predicate, a lazy generator lets you stop early without ever allocating the rest. The optional step parameter generalises the same loop to every-other-day, weekly, or any fixed cadence, and pushing the validation of step >= 1 to the top turns an infinite loop from a possibility into an immediate error. If you need weekdays only, filter the generator on dayOfWeek <= 5 rather than writing a second loop β€” composition keeps the enumeration logic in one place.

Where does enumerating dates actually show up? Rendering a month grid is the classic case: a calendar component needs the sequence of dates from the first cell to the last, and building that from PlainDate steps guarantees the grid is correct even in the week that contains a DST change, where a Date-based grid can show a duplicated or missing day. Availability and booking systems enumerate the nights in a stay to check each against an inventory. Analytics and billing bucket events by day, and the bucket keys are exactly this enumeration β€” get it wrong near midnight and a day's events scatter into the wrong column. Backfill jobs iterate a historical window one day at a time to reprocess data. In all of these the enumeration is civil-date work: the user thinks in calendar days, so the code should too.

It is worth being precise about when you do not want civil enumeration. If the question is genuinely about instants β€” 'give me the start of each day in Tokyo across this window, as absolute timestamps for a query' β€” then you enumerate PlainDate values as before but project each through the target zone with toZonedDateTime(zone).startOfDay() to get the instant that begins that calendar day there. The civil enumeration is still the backbone; you are simply attaching a zone at the end to convert each date into the moment it starts. Keeping those two steps separate β€” enumerate civil days, then zone them β€” is what keeps the loop DST-safe while still producing zoned results when you need them.

import { Temporal } from '@js-temporal/polyfill';
function* eachDate(start: Temporal.PlainDate, end: Temporal.PlainDate, step = 1) {
  if (step < 1) throw new RangeError('step must be >= 1');
  for (let d = start; Temporal.PlainDate.compare(d, end) < 0; d = d.add({ days: step })) yield d;
}
// for (const d of eachDate(a, b)) render(d);

A lazy generatorYield dates on demand with an optional stepA lazy generatorstart,end,stepvalidate stepyield cursoradd step

Verification snippet

The assertions that catch real bugs are the ones that cross a DST boundary. Enumerate a short range that spans a spring-forward Sunday β€” for a US zone, the window around the second Sunday in March β€” and assert the count is exactly the number of calendar days, with no date skipped and none repeated. Because the enumeration runs on PlainDate, the same assertion should pass identically under every TZ value, which is the property that proves the loop never leaked an instant. Add a boundary check that a range whose start equals its end yields an empty list, and that a step of two visits every other date.

Enumeration assertionsCounts stay correct across DSTAssertions that prove the edge caseMar 07–Mar 10 20263 datesspans spring-forwardno skip/repeatstart == endemptystep 2every other day

Common pitfalls

The dominant pitfall is stepping absolute time β€” new Date(cursor.getTime() + 86_400_000) β€” which drifts across DST; iterate PlainDate instead. The second is using an inclusive <= end condition, which appends one extra day and makes chained ranges overlap on their shared boundary. A third is building the whole array when you only need to test membership or find the first match, which wastes memory on long ranges where a generator would stop early. The last is forgetting that a step less than one loops forever; validate it before the loop begins.

One more design choice deserves a deliberate decision: whether the end date is included. This guide uses the half-open convention, so datesBetween('2024-03-01', '2024-03-04') yields the 1st, 2nd, and 3rd β€” three dates, the end excluded. That is the right default because it makes adjacent ranges tile cleanly and matches how until measures length. But some UIs genuinely want the inclusive list β€” 'show me every day from Monday through Friday' β€” in which case add one day to the end before enumerating, or loop with <= 0, and name the behaviour so the next reader knows it was a choice rather than an off-by-one. Whichever you pick, apply it consistently across the codebase; the expensive bugs come from one function being inclusive and the next exclusive.

If you have reached for a utility library before, this is the same operation as date-fns's eachDayOfInterval, and porting to Temporal is mostly a matter of swapping addDays(date, 1) for date.add({ days: 1 }) and the Date comparison for Temporal.PlainDate.compare. The Temporal version has one real advantage beyond dropping the dependency: because it runs on a zoneless calendar type, it cannot be knocked off by the host machine's zone, so the enumeration a report generates on a UTC server matches the one a browser renders for a user in Sydney. That determinism is the whole reason to prefer calendar stepping over millisecond arithmetic, and it is why the tests for this function should run under several TZ values and assert the output never changes.

There is also a performance dimension worth understanding, even though it rarely bites. The enumeration is linear in the number of days, and each PlainDate.add({ days: 1 }) allocates a small immutable value; for the sizes real UIs care about β€” a month, a quarter, a year β€” that cost is imperceptible. Where it can matter is a job that enumerates decades of dates in a tight loop, and there the generator form pays twice: it avoids holding tens of thousands of objects in an array at once, and it lets the consumer short-circuit as soon as it has what it needs. If you truly need only the count of days rather than the dates themselves, skip enumeration entirely and use start.until(end, { largestUnit: 'day' }).days, which computes the length directly without visiting each date. Reaching for the right tool β€” count versus enumerate, array versus generator β€” is the difference between code that scales and code that merely works on the demo data.

Finally, keep the enumeration and any per-day work separate. A common temptation is to fold formatting, filtering, and side effects into the loop body, which tangles the calendar-stepping logic with presentation concerns and makes the DST-safety harder to see and test. Yield plain PlainDate values from the enumerator, and let the caller map, filter, or format them; the stepping stays in one small, well-tested place, and everything downstream composes on top of a sequence it can trust.

Enumeration pitfalls24-hour steps and inclusive endsWrongRightnew Date + 24h loopDST driftPlainDate.add({days:1})calendar-correct<= end (inclusive)one extra day< end (half-open)exact count

Frequently Asked Questions

How do I list every date between two dates without DST bugs?

Iterate on Temporal.PlainDate and advance with .add({ days: 1 }). Because PlainDate has no time or zone, each step is exactly one calendar day, so a range crossing a daylight-saving boundary produces the correct list with no skipped or duplicated dates.

Should the end date be included in the list?

Follow the half-open convention and exclude it: loop while compare(cursor, end) < 0. That way a range from Monday to Thursday yields Monday, Tuesday, Wednesday, and two adjacent ranges never share a date.

Is it faster to return an array or a generator of dates?

For short ranges the difference is negligible and an array is simplest. A generator wins when the range is long or open-ended, or when you only need the first date that matches a condition, because it yields dates lazily and lets you stop iterating without allocating the remainder. Both use the same PlainDate stepping under the hood.

How do I list only weekdays between two dates?

Enumerate every calendar day with a PlainDate loop and filter on dayOfWeek, keeping values 1 through 5 (Monday to Friday). Filtering the enumeration keeps the day-stepping logic in one place rather than duplicating it, and it composes cleanly with a holiday set if you also need to skip public holidays.