Few things cause more bugs than dates and times. Time zones shift, daylight saving confuses calculations, and different systems use different formats. This guide cuts through the complexity. You'll learn how timestamps work, when to use which date format, and how to handle the edge cases that break production systems.
Key Takeaways
- 1Unix timestamps count seconds since January 1, 1970 UTC; JavaScript uses milliseconds
- 2Always use ISO 8601 format (YYYY-MM-DDTHH:mm:ssZ) for APIs and data storage
- 3Store dates in UTC; convert to local time only for display
- 4JavaScript months are 0-indexed (January = 0)—a common source of bugs
- 5Time zone ≠ offset: use IANA names (America/New_York) for proper DST handling
What Is a Unix Timestamp?
Scenario
Convert a human-readable date to Unix timestamp
Solution
January 1, 2025 00:00:00 UTC = 1735689600 seconds since epoch. That's 55 years × 365.25 days × 24 hours × 60 minutes × 60 seconds (approximately).
| Format | Example | When to Use |
|---|---|---|
| Seconds (Unix) | 1735689600 | APIs, databases, most backends |
| Milliseconds (JS) | 1735689600000 | JavaScript Date, modern APIs |
| Microseconds | 1735689600000000 | High-precision logging |
| Nanoseconds | 1735689600000000000 | Performance profiling |
Date Formats: ISO 8601 vs Others
| Format | Example | Notes |
|---|---|---|
| ISO 8601 (full) | 2025-01-25T14:30:00Z | Best for APIs and storage |
| ISO 8601 (offset) | 2025-01-25T14:30:00+05:30 | Includes timezone offset |
| ISO 8601 (date only) | 2025-01-25 | When time doesn't matter |
| RFC 2822 | Sat, 25 Jan 2025 14:30:00 +0000 | Email headers |
| US format | 01/25/2025 | Ambiguous—avoid in code |
| EU format | 25/01/2025 | Ambiguous—avoid in code |
JavaScript Date Handling
| Operation | Code | Result |
|---|---|---|
| Current timestamp (ms) | Date.now() | 1735689600000 |
| Current timestamp (s) | Math.floor(Date.now() / 1000) | 1735689600 |
| Parse ISO string | new Date("2025-01-25T14:30:00Z") | Date object (UTC) |
| To ISO string | date.toISOString() | "2025-01-25T14:30:00.000Z" |
| From timestamp | new Date(1735689600000) | Date object |
| To timestamp | date.getTime() | 1735689600000 |
Scenario
Parse a date string from user input or API
Solution
Always use ISO format or explicit parsing. new Date("2025-01-25") may be interpreted as UTC or local time depending on browser. Safer: new Date("2025-01-25T00:00:00Z") for UTC, or use a library like date-fns.
Time Zones: The Hard Part
- **Store in UTC** – Always store timestamps in UTC. Convert to local time only for display.
- **Offset ≠ Time Zone** – +05:30 is an offset; "Asia/Kolkata" is a time zone. Zones handle DST; offsets don't.
- **Use IANA names** – "America/New_York" not "EST". EST doesn't account for EDT (daylight saving).
- **Beware DST transitions** – Some times don't exist (clocks skip forward) or occur twice (clocks fall back).
- **Date-only values** – "2025-01-25" with no time? Store as-is or as midnight UTC. Document your choice.
| Approach | Pros | Cons |
|---|---|---|
| Store as UTC timestamp | Unambiguous, sortable | Need timezone for local display |
| Store with offset | Preserves original context | Offset may be obsolete (DST change) |
| Store with timezone name | Handles future DST | Requires timezone database |
5Common Datetime Bugs
| Bug | Cause | Fix |
|---|---|---|
| Off-by-one month | JS months are 0-indexed | Use ISO strings or libraries |
| Midnight timezone shift | new Date("2025-01-25") parsed as UTC | Always include time and zone |
| Events on wrong day | Stored UTC, displayed wrong zone | Convert with user's timezone |
| Daylight saving gaps | 2:30 AM doesn't exist during spring forward | Use libraries that handle DST |
| Leap year miscalculation | Feb 29 doesn't always exist | Use proper date math, not day+365 |
| Comparing dates as strings | "2025-1-5" < "2025-12-1" is wrong | Compare timestamps or Date objects |
Scenario
User in India (UTC+5:30) selects January 25. You store new Date("2025-01-25"). Later, user sees January 24.
Solution
The date was parsed as 2025-01-25T00:00:00 UTC. In IST, that's 2025-01-25T05:30:00, but if displayed without timezone awareness, it might show the previous day. Fix: Store with explicit timezone or as a date-only string.
Date Math & Comparisons
| Operation | Approach | Note |
|---|---|---|
| Add days | date.setDate(date.getDate() + n) | Handles month overflow correctly |
| Add months | date.setMonth(date.getMonth() + n) | May shift day (Jan 31 + 1 month = ?) |
| Difference in days | (date2 - date1) / 86400000 | Result in milliseconds, divide by ms/day |
| Compare dates | date1.getTime() < date2.getTime() | Compare timestamps, not Date objects |
| Same day check | Compare year, month, date individually | Or truncate to midnight and compare |
Convert Timestamps Instantly
Use our free Timestamp Converter to translate between Unix time and human-readable dates with timezone support.
Open Timestamp Converter7Dates in APIs & Databases
| Context | Recommended Format | Example |
|---|---|---|
| REST API request/response | ISO 8601 string | "createdAt": "2025-01-25T14:30:00Z" |
| GraphQL | ISO 8601 or custom scalar | DateTime scalar type |
| SQL databases | TIMESTAMP WITH TIME ZONE | PostgreSQL, MySQL datetime |
| NoSQL (MongoDB) | ISODate or timestamp | ISODate("2025-01-25T14:30:00Z") |
| Redis | Unix timestamp (seconds) | EXPIREAT key 1735689600 |
| Message queues | Unix timestamp (ms) | Compact, language-agnostic |
8Date Libraries: When to Use Them
| Library | Size | Best For |
|---|---|---|
| Native Date | 0 KB | Simple cases, modern browsers |
| date-fns | ~13 KB (tree-shake) | Functional approach, modular |
| Luxon | ~70 KB | Full timezone support, immutable |
| Day.js | ~2 KB + plugins | Moment.js replacement, small |
| Temporal (proposal) | 0 KB (native) | Future standard, use polyfill now |
- **Use native Date** when: Parsing ISO strings, getting current time, simple formatting
- **Use a library** when: Complex timezone logic, recurring events, relative time ("3 days ago")
- **Consider Temporal** when: Starting new projects, can use polyfill, need precision