JavaScript

Date & Timers

14 Questions

The Date object stores time internally as milliseconds since the Unix epoch (January 1, 1970 UTC). You create dates via new Date() (now), new Date(timestamp), new Date(isoString), or new Date(year, monthIndex, day). Months are zero-indexed (0=January, 11=December) — a perpetual source of off-by-one bugs.
// Current date/time
const now = new Date();

// From timestamp
new Date(0);              // Jan 1 1970 UTC (epoch)
new Date(1700000000000);  // specific moment

// From ISO string
new Date("2024-01-15");           // midnight UTC
new Date("2024-01-15T09:30:00");  // local 9:30am
new Date("2024-01-15T09:30:00Z"); // UTC 9:30am

// From components (MONTH IS ZERO-INDEXED!)
new Date(2024, 0, 15);  // Jan 15 2024 (0 = January)
new Date(2024, 11, 25); // Dec 25 2024 (11 = December)

// Reading components
const d = new Date("2024-06-15T14:30:00");
d.getFullYear(); // 2024
d.getMonth();    // 5 ← June! (0-indexed)
d.getDate();     // 15 (day of month)
d.getDay();      // 6 (day of week: 0=Sun, 6=Sat)
d.getHours();    // 14
d.getTime();     // milliseconds since epoch

Why it matters: Date creation nuances (zero-indexed months, timezone-dependent string parsing) cause subtle bugs in production. Interviewers ask about Date to gauge awareness of these non-obvious behaviors that trip up even experienced developers.

Real applications: Booking systems (appointment date selection), event scheduling, analytics dashboards (date range filtering), subscription expiry tracking, and any feature involving date/time input from users.

Common mistakes: Using new Date("2024-01-15") without a time component — parsed as UTC midnight, which displays as Jan 14 in negative UTC offset timezones. Also writing month=1 for January (must use 0). In production always use date-fns or Luxon for complex manipulation.

setTimeout(fn, delay) schedules a callback to run at least delay milliseconds later — not exactly. The callback waits in the task queue until the call stack is empty. It returns a timer ID for cancellation via clearTimeout(id). A delay of 0 still runs asynchronously — after all synchronous code and pending microtasks.
// Basic usage
const id = setTimeout(() => console.log("fires after 1s"), 1000);
clearTimeout(id); // cancel before it fires

// 0 delay is still async (macro-task queue)
console.log("1");
setTimeout(() => console.log("3"), 0);
console.log("2");
// Output: 1, 2, 3

// Pass arguments after delay
setTimeout((name) => console.log(`Hello, ${name}!`), 500, "Alice");

// Recursive setTimeout — more reliable than setInterval
// Schedules NEXT call only AFTER current completes
function poll() {
  fetchData().then(data => {
    process(data);
    setTimeout(poll, 1000); // next call after work is done
  });
}
poll();

// Promisifiable delay (useful in async functions)
const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms));
await wait(1000); // pause 1 second

Why it matters: Understanding that setTimeout(fn, 0) defers to the task queue (after microtasks) is fundamental to the JavaScript event loop model. This distinction is a core interview topic when testing concurrency understanding.

Real applications: Toast notification auto-hide, post-form-submission redirect delay, polling APIs with recursive setTimeout, debounce building block, and promisifying delay for async animation sequences.

Common mistakes: Expecting the delay to be exact (it is a minimum), forgetting to store the ID and clear it on component unmount (causes memory leaks), and passing a string like setTimeout("myFunc()", 100) which calls eval — a security vulnerability.

setInterval(fn, delay) repeatedly calls a callback approximately every delay milliseconds, returning an ID for cancellation via clearInterval(id). The critical danger: setInterval does not wait for the previous callback to finish. If the callback takes longer than the interval, callbacks start to overlap (interval drift). Prefer recursive setTimeout for async operations.
// Basic usage
const id = setInterval(() => console.log("tick"), 1000);
setTimeout(() => clearInterval(id), 5000); // stop after 5s

// PROBLEM: setInterval fires while previous is still running
setInterval(async () => {
  await fetch("/slow-api"); // takes 2s — overlap with next call!
}, 1000);

// SOLUTION: recursive setTimeout (waits for completion)
function tick() {
  fetch("/api").then(res => {
    process(res);
    setTimeout(tick, 1000); // schedule next AFTER done
  });
}
tick();

// Countdown timer pattern
let remaining = 5;
const counterId = setInterval(() => {
  console.log(remaining--);
  if (remaining < 0) clearInterval(counterId);
}, 1000);

// React: ALWAYS clear intervals in useEffect cleanup
useEffect(() => {
  const id = setInterval(() => setCount(c => c + 1), 1000);
  return () => clearInterval(id); // prevents memory leak
}, []);

Why it matters: Forgetting to clear setInterval in SPAs is one of the most common memory leak sources. After a component unmounts, the interval keeps calling setState producing "Can't update an unmounted component" warnings and stale state bugs.

Real applications: Live dashboard data refresh (every 30s), polling for notification counts, countdown timer UI components, stock price tickers, and any "refresh this data periodically" pattern where requests are fast and non-overlapping.

Common mistakes: Using setInterval for async operations that may overlap (use recursive setTimeout instead), not storing the ID so it can't be cancelled, and not cleaning up in React useEffect return / Angular ngOnDestroy / Web Component disconnectedCallback.

requestAnimationFrame(callback) schedules a callback to run before the next screen repaint — typically 60fps on a 60Hz display, automatically matching the monitor's actual refresh rate. Unlike setInterval, rAF pauses when the tab is hidden (saving battery), syncs with the GPU display cycle to prevent tearing, and is automatically throttled on low-power devices. The callback receives a DOMHighResTimeStamp.
// Basic animation loop
function animate(timestamp) {
  const progress = (timestamp / 1000) * 100; // 100px/sec
  element.style.transform = `translateX(${progress % 500}px)`;
  requestAnimationFrame(animate); // schedule next frame
}
let rafId = requestAnimationFrame(animate);
// Later: cancelAnimationFrame(rafId);

// Time-delta-based movement (frame-rate independent)
let lastTime;
function step(now) {
  if (!lastTime) lastTime = now;
  const delta = now - lastTime; // actual ms since last frame
  lastTime = now;
  move(delta * 0.2); // px/ms × elapsed
  requestAnimationFrame(step);
}
requestAnimationFrame(step);

// Fade-in utility
function fadeIn(el, duration = 500) {
  const start = performance.now();
  function frame(now) {
    el.style.opacity = Math.min((now - start) / duration, 1);
    if (now - start < duration) requestAnimationFrame(frame);
  }
  requestAnimationFrame(frame);
}

// React: cancel on unmount
useEffect(() => {
  let id = requestAnimationFrame(loop);
  return () => cancelAnimationFrame(id);
}, []);

Why it matters: Using setInterval for animations causes janky frame timing mismatches, wasted CPU in background tabs, and frame drops on high-refresh monitors. rAF is the correct tool and is foundational knowledge for frontend performance interviews.

Real applications: Canvas game loops, smooth scroll-linked animations, progress bar transitions, parallax effects, particle systems, and any visual effect that needs per-frame updates synchronized with the display.

Common mistakes: Not cancelling rAF on component unmount (animation loop continues after the component is gone), using Date.now() instead of the passed timestamp (lower precision, misses tab-hidden pausing), and using a fixed pixel-per-frame speed (animations run 2× faster on 120Hz monitors vs 60Hz).

Date.now() is a static method returning the current time as a number (milliseconds since epoch) — no object overhead, just a fast primitive. new Date() creates a Date object with formatting and getter methods. For pure timing, Date.now() is simpler. For sub-millisecond precision, use performance.now() (immune to NTP clock corrections that can make Date.now() jump backward).
// Date.now() — fast, returns a number primitive
const ts = Date.now();
console.log(typeof ts); // "number"
console.log(ts);        // e.g. 1700000000000

// new Date() — object with methods
const d = new Date();
d.getTime();     // same number as Date.now()
d.toISOString(); // "2024-01-15T09:30:00.000Z"

// Measure execution time
const start = Date.now();
heavyTask();
console.log(`Took ${Date.now() - start}ms`);

// Higher precision for micro-benchmarks
const t0 = performance.now();
heavyTask();
console.log(`Took ${(performance.now() - t0).toFixed(3)}ms`);

// Date subtraction (auto-coerces to timestamps)
const d1 = new Date("2024-01-01");
const d2 = new Date("2024-06-15");
const days = (d2 - d1) / (1000 * 60 * 60 * 24); // 166

// Token/cache expiry check (no Date object needed)
const expiry = 1700000000000;
if (Date.now() > expiry) revoke();

Why it matters: Choosing Date.now() vs new Date() vs performance.now() signals understanding of performance and correctness trade-offs. performance.now() for benchmarks is a sign of proficiency — Date.now() can jump backward due to NTP clock corrections, skewing measurements.

Real applications: Token and session expiry checks, cache TTL validation, timestamp generation for logging and event IDs, performance profiling utilities, and debounce/throttle implementations that need the current time.

Common mistakes: Using new Date().getTime() where Date.now() works (creates an unnecessary object), using Date.now() for micro-benchmarks where NTP corrections can distort results (use performance.now()), and not knowing Date arithmetic auto-calls valueOf() on both operands.

JavaScript offers several built-in date formatting options. Intl.DateTimeFormat is the most capable — locale-aware, timezone-sensitive, and fully configurable. toISOString() gives a standardized UTC string for safe storage. toLocaleDateString() gives a quick locale-sensitive display string. A combination of these eliminates Moment.js for most use cases.
const d = new Date("2024-06-15T14:30:00");

// Built-in convenience methods
d.toISOString();         // "2024-06-15T14:30:00.000Z" (always UTC)
d.toLocaleDateString();  // "6/15/2024" (locale-dependent!)
d.toLocaleString();      // "6/15/2024, 2:30:00 PM"

// Intl.DateTimeFormat — locale-aware and configurable
const fmt = new Intl.DateTimeFormat("en-US", {
  weekday: "long",
  year: "numeric",
  month: "long",
  day: "numeric"
});
fmt.format(d); // "Saturday, June 15, 2024"

// Multiple locales
new Intl.DateTimeFormat("de-DE").format(d); // "15.6.2024"
new Intl.DateTimeFormat("ja-JP").format(d); // "2024/6/15"

// With timezone conversion
new Intl.DateTimeFormat("en-US", {
  dateStyle: "medium",
  timeStyle: "short",
  timeZone: "America/New_York"
}).format(d); // shows date in NYC time

// Relative time (no Moment.js needed!)
const rtf = new Intl.RelativeTimeFormat("en", { numeric: "auto" });
rtf.format(-1, "day");  // "yesterday"
rtf.format(-3, "hour"); // "3 hours ago"
rtf.format(2, "week");  // "in 2 weeks"

Why it matters: Displaying dates correctly across locales and timezones is a real-world challenge. Correctly using Intl.DateTimeFormat and Intl.RelativeTimeFormat demonstrates awareness of modern browser APIs that eliminate heavy libraries like Moment.js (deprecated).

Real applications: Localizing date displays in multi-region apps, social media "3 hours ago" timestamps, invoice date formatting per locale, event listings showing organizer and viewer timezones side by side, and accessibility-friendly date descriptions.

Common mistakes: Calling toLocaleDateString() without a locale argument (output varies by OS/browser locale settings — always pass explicit locale), calling toISOString() and displaying it directly to users (it shows UTC which can confuse users in non-UTC timezones), and creating new Intl.DateTimeFormat instances in loops (expensive — create once and reuse with .format()).

JavaScript Date objects store time internally as UTC milliseconds since the epoch. Display methods like getHours() return local time; their getUTCHours() counterparts return UTC. This duality is the root of most date-related bugs: the same Date object shows different hour values depending on the runtime's timezone.
// ISO date-only strings are parsed as UTC midnight
const d = new Date("2024-01-15"); // midnight UTC

// In UTC-5 (New York), this is Jan 14 at 7pm local!
d.getDate();    // 14 (local time is Jan 14)
d.getUTCDate(); // 15 (UTC is Jan 15)

// FIX: include time component to force local interpretation
const local = new Date("2024-01-15T00:00:00");  // local midnight
const utc   = new Date("2024-01-15T00:00:00Z"); // UTC midnight

// Local vs UTC getters
const now = new Date();
now.getHours();     // local hours (e.g. 14)
now.getUTCHours();  // UTC hours (e.g. 19 in UTC+5)

// Get offset in minutes (positive = behind UTC, negative = ahead)
now.getTimezoneOffset(); // e.g. 300 for UTC-5

// Safe timezone-aware display
new Intl.DateTimeFormat("en-US", {
  timeZone: "America/Los_Angeles",
  dateStyle: "short",
  timeStyle: "long"
}).format(now); // converts to LA timezone

Why it matters: UTC vs. local bugs only appear in non-UTC timezones and are notoriously hard to reproduce in testing. This is a classic interview topic because many developers don't encounter it until they've caused a production outage.

Real applications: Multi-timezone event scheduling (show event in both organizer and viewer timezones), date-range API queries (send UTC boundaries), birthday reminder triggers (fire at midnight local time), and calendar applications with globally distributed users.

Common mistakes: Comparing date strings with > instead of timestamps (string comparison doesn't sort correctly across midnight/DST), creating date-only Date objects without a time component that silently shift by a day, and mixing UTC-based and local-based calculations in the same flow.

Subtracting two Date objects yields the difference in milliseconds via implicit .valueOf() coercion. Divide by the appropriate constant to convert to seconds, minutes, hours, or days. Month/year differences require careful handling of variable-length months and DST transitions (where a "day" may be 23 or 25 hours).
const start = new Date("2024-01-01");
const end   = new Date("2024-03-15");

const diffMs    = end - start;                      // milliseconds
const diffSec   = diffMs / 1000;
const diffMin   = diffMs / (1000 * 60);
const diffHours = diffMs / (1000 * 60 * 60);
const diffDays  = diffMs / (1000 * 60 * 60 * 24);
console.log(Math.floor(diffDays)); // 74

// Accurate age calculator (accounts for birthday yet this year)
function getAge(birthDate) {
  const today = new Date();
  let age = today.getFullYear() - birthDate.getFullYear();
  const m = today.getMonth() - birthDate.getMonth();
  if (m < 0 || (m === 0 && today.getDate() < birthDate.getDate())) {
    age--; // birthday hasn't happened yet this year
  }
  return age;
}
console.log(getAge(new Date("1990-06-15"))); // e.g. 34

// Days until a future date
function daysUntil(date) {
  const ms = date - Date.now();
  return Math.ceil(ms / (1000 * 60 * 60 * 24));
}
console.log(daysUntil(new Date("2025-01-01")));

// Session timeout check
const SESSION_TTL = 30 * 60 * 1000; // 30 minutes in ms
if (Date.now() - sessionStartTime > SESSION_TTL) logout();

Why it matters: Date arithmetic is required in almost every business application. The age calculator pattern (checking whether the birthday has occurred yet this year) is a classic interview problem that tests precision and edge case awareness.

Real applications: Subscription expiry countdown timers, session/JWT expiry checks, booking systems (available date ranges, duration-based pricing), "days since last login" engagement metrics, and event countdown widgets.

Common mistakes: Using integer division instead of Math.floor/ceil for day counts, not accounting for DST when a "day" is 23 or 25 hours (affects results for Europe/US date ranges), comparing Date objects with == instead of comparing their timestamps, and not copying dates before mutation.

Debounce delays execution until after the user has stopped triggering events for a set time — each new event resets the timer. Throttle ensures the function fires at most once per interval regardless of trigger frequency. Both are implemented using setTimeout/clearTimeout closures. Use debounce for search inputs; throttle for scroll, resize, and mousemove.
// DEBOUNCE — fires after user stops
function debounce(fn, delay) {
  let timer;
  return function(...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
}

const search = debounce((query) => {
  fetch(`/api/search?q=${query}`); // fires 300ms after typing stops
}, 300);
input.addEventListener("input", (e) => search(e.target.value));

// THROTTLE — fires at most once per interval
function throttle(fn, limit) {
  let lastCall = 0;
  return function(...args) {
    const now = Date.now();
    if (now - lastCall >= limit) {
      lastCall = now;
      return fn.apply(this, args);
    }
  };
}

const logScroll = throttle(() => {
  analytics.track("scroll", { y: window.scrollY });
}, 200); // at most once every 200ms
window.addEventListener("scroll", logScroll);

// React hook pattern
function useDebounce(value, delay) {
  const [dv, setDv] = useState(value);
  useEffect(() => {
    const t = setTimeout(() => setDv(value), delay);
    return () => clearTimeout(t);
  }, [value, delay]);
  return dv;
}

Why it matters: Debounce and throttle are canonical coding interview questions (alongside once() and memoize) that test closure, higher-order function, and timer knowledge. Implementing them from scratch is a common whiteboard or take-home requirement.

Real applications: Search autocomplete (debounce API calls by 300ms), infinite scroll with intersection observer (throttle checks), window resize handler (debounce layout recalculation), game input (throttle actions), and analytics scroll depth tracking.

Common mistakes: Confusing which to use — "wait until calm" = debounce, "fire regularly but not too fast" = throttle. Not preserving this context with fn.apply(this, args). In production, use Lodash's _.debounce/_.throttle which handle leading/trailing edge options and cancel/flush correctly.

JavaScript timers are not exact — the delay parameter is a minimum, not a guaranteed wait time. The callback only runs when the call stack is empty after the delay passes, so a busy main thread delays execution. Browsers also apply a minimum 4ms clamp on nested timers (after 5 levels of nesting) and may clamp background tab timers to 1000ms.
// Timer fires LATER if main thread is busy
const start = performance.now();
setTimeout(() => {
  const actual = performance.now() - start;
  console.log(`Expected 100ms, got ${actual.toFixed(2)}ms`);
  // May print 103ms, 112ms, etc.
}, 100);
for (let i = 0; i < 1e8; i++) {} // busy loop delays timer

// Self-correcting interval (compensates for drift)
function accurateInterval(fn, delay) {
  let expected = Date.now() + delay;
  function step() {
    const drift = Date.now() - expected;
    fn();
    expected += delay;
    setTimeout(step, Math.max(0, delay - drift)); // adjust for drift
  }
  setTimeout(step, delay);
}

// Pause polling when tab is hidden
document.addEventListener("visibilitychange", () => {
  if (document.hidden) pausePolling();
  else resumePolling();
});

// Use performance.now() for frame-accurate elapsed time
let lastFrame = performance.now();
function loop(now) {
  const delta = now - lastFrame; // actual ms since last frame
  lastFrame = now;
  update(delta);
  requestAnimationFrame(loop); // more accurate than setInterval for animation
}

Why it matters: Understanding timer imprecision is critical for clock-sensitive features and animations. The 4ms clamping and background tab throttling affect real production behavior and distinguish candidates who have debugged timing bugs from those who haven’t.

Real applications: Slideshow auto-advance (user expects consistent timing), timer-based game scoring (score within X seconds), session timeout warnings that must fire on schedule, stopwatch accuracy, and audio/video synchronization.

Common mistakes: Building a stopwatch with setInterval and showing interval × count (drifts over time — use accumulated delta instead), not yielding to rendering by running a tight loop in a setInterval callback, and expecting timers to fire while a CPU-blocking task is executing on the main thread.

The Intl API provides locale-aware formatting built into modern engines — no library needed. Key classes: Intl.DateTimeFormat (dates/times), Intl.NumberFormat (numbers/currencies), Intl.Collator (sorting), Intl.RelativeTimeFormat ("2 days ago"), and Intl.ListFormat ("Alice, Bob, and Charlie"). Together they cover what Moment.js was commonly used for.
// Numbers and currency
new Intl.NumberFormat("en-US", {
  style: "currency", currency: "USD"
}).format(1234567.89); // "$1,234,567.89"

new Intl.NumberFormat("de-DE", {
  style: "currency", currency: "EUR"
}).format(1234.56); // "1.234,56 €"

// Relative time
const rtf = new Intl.RelativeTimeFormat("en", { numeric: "auto" });
rtf.format(-1, "day");  // "yesterday"
rtf.format(-3, "hour"); // "3 hours ago"
rtf.format(2, "week");  // "in 2 weeks"

// Locale-aware sort (handles accented chars, Unicode)
const names = ["\u00c5ngstr\u00f6m", "Alice", "Bob"];
names.sort(new Intl.Collator("sv").compare);
// Swedish ordering

// List format
new Intl.ListFormat("en", {
  style: "long", type: "conjunction"
}).format(["Alice", "Bob", "Charlie"]);
// "Alice, Bob, and Charlie"

// Plural rules
const pr = new Intl.PluralRules("en-US");
pr.select(1);  // "one"  → "1 item"
pr.select(2);  // "other" → "2 items"

Why it matters: The Intl API replaces Moment.js for most use cases, eliminating a heavy dependency. Knowing it demonstrates awareness of modern JavaScript capabilities and shows that browser-native solutions now cover most i18n requirements.

Real applications: Multi-currency e-commerce price display, social media post timestamps, locale-appropriate number formatting in analytics, locale-aware sorting in data tables, and accessible human-readable lists for screen readers.

Common mistakes: Instantiating new Intl.DateTimeFormat() inside a loop (expensive — create once, reuse with .format()), using toLocaleString() without arguments (varies by OS/browser settings) instead of passing an explicit locale, and not knowing that Intl.Collator handles accent and case sensitivity correctly while plain sort() does not.

Always clear timers when a component unmounts or scope is torn down. A running timer holds a reference to its callback’s closure, preventing garbage collection — if that closure references DOM nodes, state, or component instances, they also stay in memory. Not clearing timers is one of the most common sources of memory leaks in SPAs.
// React: return cleanup from useEffect
useEffect(() => {
  const timer = setTimeout(() => {
    setNotification("Session expiring soon!");
  }, 25 * 60 * 1000);
  return () => clearTimeout(timer); // clears on unmount
}, []);

// setInterval cleanup
useEffect(() => {
  const id = setInterval(() => {
    setCount(c => c + 1); // functional update avoids stale closure
  }, 1000);
  return () => clearInterval(id);
}, []);

// requestAnimationFrame cleanup
useEffect(() => {
  let rafId;
  function loop() {
    renderFrame();
    rafId = requestAnimationFrame(loop);
  }
  rafId = requestAnimationFrame(loop);
  return () => cancelAnimationFrame(rafId);
}, []);

// Vanilla / Web Components
class MyWidget extends HTMLElement {
  connectedCallback() {
    this.#id = setInterval(() => this.refresh(), 5000);
  }
  disconnectedCallback() {
    clearInterval(this.#id); // must always clean up!
  }
}

Why it matters: React’s "Can’t perform a state update on an unmounted component" warning is almost always a timer that wasn’t cleared. This is tested in frontend interviews because it reveals understanding of component lifecycle and memory management.

Real applications: Session timeout warnings, auto-save notifications, rate-limited polling hooks, canvas animation cleanup, components that show countdown timers or live progress indicators, and chat notification polling.

Common mistakes: Storing the timer ID in a regular variable that gets recreated on re-render (must use useRef in React), using global variables for IDs when multiple component instances need independent timers, and not cancelling RAF loops that continue running after the animation is "done".

queueMicrotask(fn) schedules a callback as a microtask — after the current JavaScript finishes but before any macro-tasks (setTimeout, I/O). It uses the same microtask queue as resolved Promises, so queueMicrotask callbacks run before setTimeout(fn, 0) even though both "defer" work.
console.log("1 - sync");
setTimeout(() => console.log("5 - setTimeout"), 0);
Promise.resolve().then(() => console.log("3 - Promise .then"));
queueMicrotask(() => console.log("2 - queueMicrotask"));
console.log("4 - sync end");

// Output ORDER:
// 1 - sync
// 4 - sync end
// 2 - queueMicrotask   ← microtask queue
// 3 - Promise .then    ← also microtask queue (FIFO)
// 5 - setTimeout       ← macro-task queue (last)

// Use case: batch multiple synchronous updates into one flush
let pending = [];
function scheduleFlush() {
  if (pending.length === 1) { // schedule only once
    queueMicrotask(() => {
      const items = pending.splice(0);
      items.forEach(applyUpdate);
    });
  }
}
function update(change) {
  pending.push(change);
  scheduleFlush();
}
update(a); // one microtask scheduled
update(b); // appended to pending
update(c); // all three flushed in one microtask

// Prefer over Promise.resolve().then() for clarity
queueMicrotask(flush); // clear intent

Why it matters: Understanding the microtask vs macro-task distinction is essential for JavaScript concurrency interviews. The execution order of Promises, queueMicrotask, and setTimeout is a very common interview question that catches many candidates.

Real applications: Virtual DOM frameworks that batch multiple state changes into one render microtask, scheduler implementations that defer work within the same event loop tick, and plugin hooks that need to run after current sync initialization but before rendering.

Common mistakes: Creating an infinite microtask loop by queuing another microtask inside the callback — the queue never empties and the page freezes. Also using queueMicrotask when setTimeout is more appropriate (when you need to yield to rendering between the current work and deferred work).

JavaScript’s Date has no built-in add/subtract methods. The correct approach uses the set*() family (setDate, setMonth, setHours) which correctly handles overflow — adding 5 days to January 30 auto-advances to February. For time adjustments, add milliseconds directly to the timestamp for precision and simplicity.
// Add days — setDate handles month overflow
const d = new Date("2024-01-30");
d.setDate(d.getDate() + 5);
console.log(d.toDateString()); // "Sat Feb 03 2024" (auto-wrapped!)

// NON-MUTATING add (important for React state)
function addDays(date, days) {
  const result = new Date(date); // copy first!
  result.setDate(result.getDate() + days);
  return result;
}
const tomorrow  = addDays(new Date(), 1);
const nextWeek  = addDays(new Date(), 7);
const lastWeek  = addDays(new Date(), -7); // negative = subtract

// Add months (beware: Jan 31 + 1 month = Mar 2, not Feb 28!)
function addMonths(date, months) {
  const result = new Date(date);
  result.setMonth(result.getMonth() + months);
  return result;
}

// Add time with millisecond arithmetic
const inTwoHours  = new Date(Date.now() + 2 * 60 * 60 * 1000);
const inThirtyMin = new Date(Date.now() + 30 * 60 * 1000);

// Expiry date for JWT/session
const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000); // +1 day

Why it matters: Date arithmetic edge cases (month overflow, DST transitions, varying month lengths) routinely cause production bugs in booking and scheduling systems. Knowing to copy the date before mutating is important for immutable React state.

Real applications: Subscription renewal dates (add 1 month/year), session and JWT expiry (add hours to now), booking availability windows (add days to check-in to calculate check-out search range), reminder systems, and recurring event generation.

Common mistakes: Mutating the original Date object directly instead of copying first, trusting month arithmetic without overflow understanding (Jan 31 + 1 month gives Mar 2, not Feb 28 or 29 — use date-fns addMonths for correct behavior), and not accounting for DST when adding hours near DST transition days.