Mesrai
Back to blog
// essayTechnical Deep Dive

Off-By-One in Subscription Trial Day Calculation

Real PR catch: setDate trial calc — mutates source, drifts at DST. date-fns addDays fix.

Mesrai TeamAugust 22, 20268 min read

A 14-day-trial calculator. Mutates the start date, adds 14 days. Looked fine until DST transitions in non-IST regions caused trials to end at 23:00 or 01:00 instead of midnight, sometimes coinciding with off-by-one display.

The vulnerable diff

tsbilling/trial.ts
// billing/trial.ts
function calculateTrialEnd(start: Date): Date {
  // BUG #1: setDate mutates start. Caller's date object changes.
  // BUG #2: 14 days across a DST boundary may give 14×24h not 14 calendar days.
  return new Date(start.setDate(start.getDate() + 14));
}

What is wrong

Two related bugs. `Date.setDate` mutates the date object — surprising to callers expecting immutability. And adding days via `setDate(getDate() + N)` is calendar-aware (handles month boundaries) but still operates in the local timezone — DST transitions can shift the time-of-day. The fix is a date library (date-fns, Luxon, Temporal) that handles both immutability and DST correctly.

The attack

Symptom on a DST-observing region:

textexploit
start = 2026-03-08 12:00:00 (US/Eastern, before DST)
trialEnd computed naively: 2026-03-22 11:00:00 (after DST shifted by 1h)
Display logic shows "trial ends Sat Mar 21" (calculated days remaining = 13)

Customer sees 13 days. Support tickets follow.

Mesrai's review comment

textreview-comment.txt
mesraipilot · Bot · reviewed 1 min ago

[mesrai] [code-review] [Logic] [Date-Math] [medium]

Two issues:

  1. setDate mutates start — surprises callers.
  2. 14 days across DST shifts time-of-day by ±1 hour, can cause
     off-by-one displayed days.

Use a date library:

  import { addDays } from "date-fns";
  function calculateTrialEnd(start: Date): Date {
    return addDays(start, 14);  // immutable, DST-aware
  }

Or Temporal (now standard):

  function calculateTrialEnd(startIso: string, tz: string) {
    return Temporal.ZonedDateTime.from(`${startIso}[${tz}]`)
      .add({ days: 14 })
      .toString();
  }

Pin the user's timezone with the date if the trial end matters at
calendar-day precision (it usually does for billing).

The fix

tsbilling/trial.ts (fixed)
// billing/trial.ts — fixed
import { addDays } from "date-fns";

function calculateTrialEnd(start: Date): Date {
  return addDays(start, 14);
}

Immutable, DST-aware addition. Cluster 7 post 70 also applies — store the user's timezone and apply it where calendar-day precision matters.

Why human review missed it

Date arithmetic bugs are subtle and don't reproduce in dev (no DST transition in the test window). Mesrai catches `setDate(getDate() + N)` patterns and recommends the date-library equivalent.

Related rules + further reading

Mesrai rule pack: logic/date-mutate-and-dst — flags setDate / setMonth mutations and recommends date-library add.

date-fns: Why Immutability Matters.

Common in trial / subscription / scheduling code.

Takeaway

Use a date library. Immutable + DST-aware. setDate mutates; the math drifts at DST. Mesrai catches the pattern.

// try

See it on your next PR.

Free for individuals. Install in two minutes. Mesrai reviews every commit.