Time is deceptively simple — until you try to calculate it correctly.
What started as a small weekend experiment turned into 16+ date and time utilities running entirely in the browser. Along the way, I ran into classic problems:
- Daylight Saving Time bugs
- Off-by-one errors
- Timezone inconsistencies
- ISO week calculation quirks
- Performance vs library tradeoffs
- Privacy considerations
This article walks through the technical decisions behind building reliable date tools using vanilla JavaScript only — and the lessons that might save you from subtle production bugs.
*The Real Problem With “Days Between Dates”
*
Most developers try something like:
const diff = (end - start) / (1000 * 60 * 60 * 24);
It works… until it doesn’t.
*The DST Problem
*
If a date range crosses a Daylight Saving Time boundary, you might get:
- 6.958333 days
- 7.041666 days
- 6 instead of 7
- 8 instead of 7
Why? Because **not **every day has 24 hours.
The solution is not to divide milliseconds blindly.
Instead, I switched to a UTC day index approach:
const _utcDayIndex = (d) => {
return Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()) / 86400000;
};
Then:
const startIdx = _utcDayIndex(date1);
const endIdx = _utcDayIndex(date2);
const diff = endIdx - startIdx;
This works because:
- It ignores time components
- It normalizes everything to UTC midnight
- It makes DST irrelevant
- The difference is always an integer
This single change removed multiple edge-case bugs.
*Business Days: Another Trap
*
Calculating business days sounds trivial:
“Count weekdays between two dates.”
But if you loop using local time and increment with setDate(getDate() + 1), DST can shift time values and introduce subtle errors.
Instead, I iterate using UTC day indices:
for (let dayIdx = startIdx; dayIdx < endIdx; dayIdx++) {
const dt = new Date(dayIdx * 86400000);
const dow = dt.getUTCDay();
if (dow !== 0 && dow !== 6) count++;
}
Key decisions:
- Use
getUTCDay()notgetDay() - Use a numeric day index
- Avoid mutating original dates
This guarantees consistency regardless of user timezone.
*Age Calculation Is Harder Than It Looks
*
A naive age calculation:
const years = now.getFullYear() - birth.getFullYear();
That’s wrong for users whose birthday hasn’t happened yet this year.
A correct implementation must:
- Subtract years
- Adjust months if needed
- Adjust days if needed
- Handle month-length differences
Example logic:
if (days < 0) {
months--;
const lastMonth = new Date(referenceYear, referenceMonth, 0);
days += lastMonth.getDate();
}
if (months < 0) {
years--;
months += 12;
}
This ensures:
- 29 Feb birthdays behave correctly
- Month boundaries don’t break
- Output is calendar-accurate
*Why I Didn’t Use date-fns or Moment.js
*
This was intentional.
Goals:
⚡ Near-zero bundle weight
🔒 All calculations client-side
🚀 Instant load on mobile
🧠 Full control over edge cases
Using vanilla JavaScript:
- Keeps the tools extremely fast
- Avoids dependency updates
- Forces deeper understanding of time logic
For small, deterministic operations, native Dateis enough — if you handle it carefully.
*Privacy-First Design
*
Every calculation runs locally in the browser.
No:
- Birth dates stored
- Tracking of date inputs
- Server processing
- External APIs
The only analytics (optional, consent-based) are handled via Cookiebot-controlled scripts.
For date tools especially (age, birthdays, payroll hours), keeping data local builds trust.
*Architecture Notes
*
The project structure is intentionally simple:
- Static HTML pages
- Vanilla JS utilities
- Shared helper module
- No framework
- No build step
Core utilities include:
- DST-safe days between
- Business day calculator
- ISO-8601 week number
- Time duration formatter
- Add/subtract date logic
- Age breakdown calculator
All of this powers the full suite of tools available on the project.
Unexpected Lessons
**
**1. Timezone Bugs Are Invisible Until Production
Everything works on your machine… until someone in Australia reports an issue.
2. Midnight Is Dangerous
Creating dates at midnight can cause DST rollover problems.
Using noon when parsing input avoids this:
new Date(year, month - 1, day, 12, 0, 0, 0);
This eliminates many edge cases.
*3. Simple Tools Are SEO Gold
*
While building the tools, I discovered something interesting:
Very specific utilities:
- Age in days
- Birth year from age
- How old will I be in 2035
- Chronological age calculator
target extremely specific search intent.
Sometimes micro-tools outperform generic ones.
*The Result
*
What began as a technical experiment turned into a full collection of browser-based date utilities:
The focus remains:
- Accuracy
- Performance
- Privacy
- Zero dependencies
*Final Thoughts
*
If you’re building anything involving dates:
- Don’t divide milliseconds blindly
- Normalize to UTC day indices
- Be careful with DST boundaries
- Treat month arithmetic cautiously
- Prefer deterministic logic over convenience
Time is simple — until it isn’t.
And most production bugs around time are subtle, embarrassing, and avoidable.
Top comments (0)