Working with Dates and Times in JavaScript: The Practical Guide
Dates in JS are notoriously confusing. Here's how to handle them correctly.
The Problem with Date
// ❌ The old way (still works, but painful)
const now = new Date();
const year = now.getFullYear(); // 2026
const month = now.getMonth() + 1; // 5 (months are 0-indexed!)
const day = now.getDate(); // 16
const hours = now.getHours(); // Local time
// Parsing dates is a minefield:
new Date('2026-05-16'); // OK (ISO format)
new Date('05/16/2026'); // Depends on locale!
new Date('May 16, 2026'); // Depends on locale!
new Date('2026-05-16T00:00:00Z'); // UTC — different from local time!
// Math with dates:
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1); // Mutates the original date!
Solution 1: date-fns (Lightweight)
npm install date-fns
import { format, addDays, subMonths, isAfter, isWeekend } from 'date-fns';
const now = new Date();
// Format (no more manual string building!)
format(now, 'yyyy-MM-dd'); // "2026-05-16"
format(now, 'MMMM do, yyyy'); // "May 16th, 2026"
format(now, 'h:mm a'); // "7:30 PM"
format(now, 'EEEE, MMMM d, yyyy h:mm a');
// "Friday, May 16, 2026 7:30 PM"
// Add/subtract (immutable — returns new date!)
const nextWeek = addDays(now, 7);
const lastMonth = subMonths(now, 1);
// Compare
isAfter(nextWeek, now); // true
isWeekend(now); // false or true
// Difference
import { differenceInDays, differenceInHours } from 'date-fns';
differenceInDays(new Date('2026-06-01'), now);
differenceInHours(new Date(), somePastDate);
Solution 2: Temporal API (Future Standard)
// Coming to JS engines soon! Check browser support.
import { Temporal } from '@js-temporal/polyfill';
const now = Temporal.Now.plainDateTimeISO();
// 2026-05-16T19:30:00
const birthday = Temporal.PlainDate.from({
year: 1995,
month: 8,
day: 15,
});
birthday.add({ years: 30 }); // 2025-08-15
birthday.until(Temporal.Now.plainDateISO()).days; // Days until/since
// Time zones handled properly!
const nyTime = Temporal.Now.zonedDateTimeISO('America/New_York');
const tokyoTime = nyTime.withTimeZone('Asia/Tokyo');
// No more UTC vs local confusion!
Common Patterns
Relative Time ("3 hours ago")
function timeAgo(date) {
const seconds = Math.floor((Date.now() - date.getTime()) / 1000);
if (seconds < 60) return `${seconds}s ago`;
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`;
if (seconds < 2592000) return `${Math.floor(seconds / 86400)}d ago`;
if (seconds < 31536000) return `${Math.floor(seconds / 2592000)}mo ago`;
return `${Math.floor(seconds / 31536000)}y ago`;
}
timeAgo(new Date(Date.now() - 5000)); // "5s ago"
timeAgo(new Date(Date.now() - 7200000)); // "2h ago"
timeAgo(new Date(Date.now() - 86400000)); // "1d ago"
Countdown Timer
function countdown(targetDate) {
const now = new Date();
const diff = targetDate - now;
if (diff <= 0) return { days: 0, hours: 0, minutes: 0, seconds: 0 };
return {
days: Math.floor(diff / (1000 * 60 * 60 * 24)),
hours: Math.floor((diff / (1000 * 60 * 60)) % 24),
minutes: Math.floor((diff / (1000 * 60)) % 60),
seconds: Math.floor((diff / 1000) % 60),
};
}
// Usage with setInterval for live countdown
const target = new Date('2026-12-31T23:59:59');
setInterval(() => {
console.log(countdown(target));
}, 1000);
Is This Date Valid?
function isValidDate(date) {
return date instanceof Date && !isNaN(date.getTime());
}
isValidDate(new Date('2026-02-30')); // false (Feb 30 doesn't exist)
isValidDate(new Date('invalid')); // false
isValidDate(new Date()); // true
Format Duration (ms → readable)
function formatDuration(ms) {
if (ms < 1000) return `${ms}ms`;
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
if (ms < 3600000) return `${Math.floor(ms / 60000)}m ${Math.round((ms % 60000) / 1000)}s`;
if (ms < 86400000) return `${Math.floor(ms / 3600000)}h ${Math.round((ms % 3600000) / 60000)}m`;
return `${Math.floor(ms / 86400000)}d`;
}
formatDuration(1500); // "1.5s"
formatDuration(65000); // "1m 5s"
formatDuration(3661000); // "1h 1m"
formatDuration(90000000); // "1d"
Business Days Only (Skip Weekends)
function addBusinessDays(date, days) {
const result = new Date(date);
let added = 0;
while (added < days) {
result.setDate(result.getDate() + 1);
const day = result.getDay();
if (day !== 0 && day !== 6) added++; // Skip Sunday (0) and Saturday (6)
}
return result;
}
addBusinessDays(new Date('2026-05-15'), 3);
// Skips weekend → returns Monday or Tuesday
Database ↔ JavaScript
// PostgreSQL timestamp → JS Date
const dbTimestamp = '2026-05-16T19:30:00.000Z';
const jsDate = new Date(dbTimestamp);
// JS Date → ISO string for API/database
const isoString = new Date().toISOString();
// "2026-05-16T11:30:00.000Z" (always UTC!)
// Display in user's timezone
const localString = new Date().toLocaleString('en-US', {
timeZoneName: 'short',
});
// "5/16/2026, 7:30:00 PM GMT+8"
// User-friendly format
const friendly = new Date().toLocaleDateString('en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
});
// "Friday, May 16, 2026"
Timezone Handling
// Get user's timezone
Intl.DateTimeFormat().resolvedOptions().timeZone;
// "Asia/Shanghai" or "America/New_York" etc.
// Convert between timezones
function convertTZ(date, tz) {
return new Date(date.toLocaleString('en-US', { timeZone: tz }));
}
// Store as UTC, display as local
const serverTime = new Date().toISOString(); // Always store UTC!
const localDisplay = new Date(serverTime).toLocaleString();
Quick Reference
| Task | Native JS | date-fns |
|---|---|---|
| Now | new Date() |
— |
| Format | Manual | format(d, pattern) |
| Add days |
setDate() (mutates!) |
addDays(d, n) |
| Diff | Manual math | differenceInDays(a, b) |
| Parse | new Date(str) |
parse(str) |
| Compare |
<, >
|
isBefore, isAfter
|
| Start of month | Manual | startOfMonth(d) |
| End of month | Manual | endOfMonth(d) |
| Is valid? | !isNaN(d.getTime()) |
isValid(d) |
How do you handle dates in your projects? Any horror stories?
Follow @armorbreak for more JavaScript content.
Top comments (0)