JavaScript Dates: Working with Time the Right Way

The Date object lets you create, read, format, and do math with dates and times. Learn how it really works, the timezone traps that bite everyone, and the modern way to handle dates safely.

Published July 31, 202610 min readBy ACY Partner Indonesia
JavaScript dates — the Date object, timestamps, and timezones
300 × 250Ad Space AvailablePlace your ad here

Almost every app deals with time at some point. A blog post has a publish date. An order has a timestamp. A countdown ticks toward a deadline. A “last seen 3 minutes ago” label updates as you watch. All of that runs through JavaScript’s built-in Date object — and Date has a reputation for being one of the most confusing parts of the language.

The good news: most of that confusion comes from a handful of specific quirks. Once you know what they are, dates stop feeling like a minefield. This article walks through creating dates, reading their parts, formatting them for humans, doing arithmetic, and — most importantly — the timezone and parsing traps that catch nearly everyone the first time. You’ll want to be comfortable with objects before diving in, since a Date is itself a special kind of object.

What a Date actually is

Under the hood, a JavaScript Date is just a single number: the count of milliseconds since a fixed moment called the Unix epoch — midnight UTC on January 1, 1970. Every date you create, no matter how it looks on screen, is stored internally as that one number.

That’s the most important idea in this whole article. A Date doesn’t store “March 15th in Jakarta.” It stores an exact instant in time as a millisecond count, and then shows it to you in your local timezone when you ask. Keep that mental model and most of the weird behavior starts to make sense.

const now = new Date();
console.log(now.getTime());   // e.g. 1781827200000 — milliseconds since 1970

Creating a Date

You create a date with the new Date() constructor, and it behaves differently depending on what you pass it. There are four main forms.

No arguments — the current moment:

const now = new Date();
console.log(now);   // the date & time right now

A timestamp (milliseconds since the epoch):

const epoch = new Date(0);            // Jan 1, 1970, 00:00:00 UTC
const later = new Date(1781827200000); // some moment in 2026

Numeric parts (year, month, day, …):

const d = new Date(2026, 6, 31, 14, 30, 0);
// July 31, 2026, 14:30:00 — in LOCAL time

A date string:

const d = new Date("2026-07-31");
const d2 = new Date("2026-07-31T14:30:00");

Each form has a catch worth knowing, and the numeric-parts form has the most infamous one of all.

Months are zero-based

When you build a date from numbers, the month is counted from 0, not 1. So 0 is January, 1 is February, and 11 is December. This means new Date(2026, 6, 31) is July 31st, not June. Days of the month, confusingly, do start at 1 — only the month is zero-based. This single quirk is responsible for more date bugs than anything else in JavaScript. When a date comes out exactly one month off, this is almost always why.

Reading the parts of a date

Once you have a Date, you pull pieces out of it with get methods. These all return the values in your local timezone:

const d = new Date(2026, 6, 31, 14, 30, 0);

d.getFullYear();   // 2026
d.getMonth();      // 6  (July — remember, zero-based!)
d.getDate();       // 31 (day of the month, 1–31)
d.getDay();        // 5  (day of the WEEK: 0 = Sunday, 6 = Saturday)
d.getHours();      // 14
d.getMinutes();    // 30
d.getSeconds();    // 0

A couple of these names trip people up. getDate() gives you the day of the month (a number from 1 to 31), while getDay() gives you the day of the week as a number where Sunday is 0. They sound interchangeable but they’re completely different — mixing them up is a classic mistake.

There’s also getTime(), which returns that raw millisecond timestamp. It’s the cleanest way to compare two dates, as we’ll see shortly.

Formatting dates for humans

A raw Date printed to the console looks technical and ugly. To show something readable, you have a few options — and the modern one is much better than the old ones.

The quick built-in methods give you fixed formats:

const d = new Date(2026, 6, 31, 14, 30);

d.toString();          // "Fri Jul 31 2026 14:30:00 GMT+0700 ..."
d.toDateString();      // "Fri Jul 31 2026"
d.toISOString();       // "2026-07-31T07:30:00.000Z"  (always UTC)
d.toLocaleDateString();// "7/31/2026"  (depends on the user's locale)

toISOString() is special and worth remembering: it always returns the date in UTC using the standard ISO 8601 format. That’s the format you want when storing dates or sending them to a server, because it’s unambiguous.

For showing dates to actual people, the best tool is Intl.DateTimeFormat (or its shortcut, toLocaleDateString with options). It formats dates correctly for any language and region without you hand-assembling strings:

const d = new Date(2026, 6, 31, 14, 30);

const formatted = new Intl.DateTimeFormat("en-US", {
  weekday: "long",
  year: "numeric",
  month: "long",
  day: "numeric",
}).format(d);

console.log(formatted);   // "Friday, July 31, 2026"

Stop hand-building date strings

It’s tempting to glue a date together yourself with something like `${day}/${month}/${year}`. Resist it. You’ll fight zero-padding, month names, locale differences, and edge cases forever. Intl.DateTimeFormat handles all of that — translated month names, the right separators, 12- vs 24-hour clocks — for free, and it’s built into every modern browser. Reach for it whenever you need to display a date to a user.

Doing math with dates

Date arithmetic is where the millisecond-number idea pays off. Because every date is really a number, you can compare and subtract them.

Comparing two dates: convert both to their timestamps with getTime() (or the unary +) and compare the numbers.

const start = new Date("2026-01-01");
const end = new Date("2026-12-31");

console.log(end.getTime() > start.getTime());   // true — end is later

Finding the difference between two dates: subtract their timestamps to get milliseconds, then convert to whatever unit you want.

const start = new Date("2026-07-01");
const end = new Date("2026-07-31");

const diffMs = end - start;                       // difference in milliseconds
const diffDays = diffMs / (1000 * 60 * 60 * 24);  // ms → seconds → minutes → hours → days

console.log(diffDays);   // 30

That 1000 * 60 * 60 * 24 is the number of milliseconds in a day, and you’ll write it (or a constant for it) often.

Adding or subtracting time is where things get slightly tricky. The clean, reliable way is to use the set methods, because they automatically handle rolling over month and year boundaries:

const d = new Date(2026, 6, 31);   // July 31, 2026

d.setDate(d.getDate() + 7);        // add 7 days
console.log(d);                    // August 7, 2026 — it rolled into the next month correctly

Notice that adding 7 days to July 31 didn’t produce “July 38” — it correctly advanced into August. The set methods do that math for you, including leap years, which is exactly why you should prefer them over fiddling with raw millisecond sums for adding days, months, or years.

The mutability trap

Here’s a subtle gotcha that causes real bugs: Date objects are mutable. Methods like setDate() and setMonth() don’t return a new date — they change the original date in place.

const original = new Date(2026, 6, 31);
const copy = original;       // NOT a copy — same object!

copy.setDate(1);
console.log(original.getDate());   // 1  — the "original" changed too!

Assigning a Date doesn't copy it

Because a Date is an object, writing const copy = someDate just makes copy point at the same date. Mutating one mutates both. If you need a genuine, independent copy before modifying it, create a fresh date from the timestamp: const copy = new Date(original.getTime()). Forgetting this is a common source of “why did my other variable change?” bugs — and it’s the same reference behavior you may have met with objects and arrays generally.

The timezone trap

This is the one that bites everyone eventually, so it deserves its own section. Remember that a Date stores an instant in UTC and displays it in local time. That gap is where surprises hide.

The most notorious example is parsing a date-only string:

const d = new Date("2026-07-31");
console.log(d.toISOString());   // "2026-07-31T00:00:00.000Z"

A string in the "YYYY-MM-DD" format is parsed as midnight UTC. If you’re in a timezone ahead of UTC — say Jakarta, UTC+7 — then that same instant is 7 a.m. on July 31 locally, which seems fine. But in a timezone behind UTC, like New York, midnight UTC on July 31 is actually 7 p.m. on July 30. So new Date("2026-07-31").getDate() can return 30 for some of your users. A date that looks correct on your machine silently shifts by a day for someone else.

Date-only strings are parsed as UTC; date-time strings as local

There’s an inconsistency baked into the spec that’s worth memorizing. new Date("2026-07-31") (date only) is treated as UTC. But new Date("2026-07-31T14:30") (with a time, and no Z or offset) is treated as local time. Same-looking strings, different rules. To avoid the whole mess, either always include an explicit timezone offset (the Z for UTC, or +07:00), or build the date from numeric parts with new Date(year, month, day), which is always interpreted locally.

The practical takeaway: when a date is one day off and only for some users, suspect timezone-based parsing. Store and transmit dates as full ISO strings in UTC, and only convert to local time at the moment you display them.

A realistic example

Let’s tie it together with a small “time ago” helper — the kind of thing that turns a timestamp into “3 days ago”, which you’ve seen on countless sites:

function timeAgo(date) {
  const seconds = Math.floor((new Date() - date) / 1000);

  const intervals = {
    year: 31536000,
    month: 2592000,
    day: 86400,
    hour: 3600,
    minute: 60,
  };

  for (const [unit, secondsInUnit] of Object.entries(intervals)) {
    const count = Math.floor(seconds / secondsInUnit);
    if (count >= 1) {
      return `${count} ${unit}${count > 1 ? "s" : ""} ago`;
    }
  }
  return "just now";
}

const posted = new Date("2026-07-28T10:00:00Z");
console.log(timeAgo(posted));   // e.g. "3 days ago"

This pulls together everything from the article: subtracting two dates to get a difference in milliseconds, converting that to larger units, and looping over an object’s entries (which you saw with loops) to pick the right one. It’s a genuinely useful function you could drop into a real project.

When the built-in Date isn’t enough

For a lot of work, the native Date is perfectly fine. But once you’re doing heavy timezone juggling, complex parsing, or lots of formatting, its rough edges add up. Historically people reached for libraries like Moment.js, but Moment is now considered legacy. Modern lightweight options like date-fns and Day.js cover most needs.

The bigger news is Temporal, a new built-in API designed to fix Date’s long-standing problems — immutable objects, clear separation between dates and times, and first-class timezone support. It’s the future of dates in JavaScript and is rolling out across browsers. If you’re starting fresh today, the native Date is still what you’ll use most, but it’s worth knowing Temporal exists for when it becomes widely available.

Wrapping up

Dates feel intimidating mainly because of a few sharp edges. Know them and you’re set:

  • A Date is internally a number of milliseconds since the Unix epoch (Jan 1, 1970 UTC). It stores an instant and displays it in local time.
  • Create dates with new Date() — no args for now, a timestamp, numeric parts, or a string. Months are zero-based (6 = July).
  • Read parts with get methods. Watch the pair: getDate() is day-of-month (1–31); getDay() is day-of-week (0 = Sunday).
  • Format for humans with Intl.DateTimeFormat rather than building strings by hand. Use toISOString() for storing and transmitting.
  • Do math by treating dates as numbers: subtract timestamps for a difference, and use set methods to add/subtract days safely.
  • Date objects are mutable — copy with new Date(d.getTime()) before changing one.
  • Beware the timezone trap: date-only strings parse as UTC, date-time strings as local. Store in UTC, display in local.

Get comfortable with these rules and the “scary” reputation of JavaScript dates fades fast. You now have everything you need to build countdowns, timestamps, and human-friendly date displays with confidence.

Tags:javascriptfrontenddatetimeintermediate
728 × 90Ad Space AvailablePlace your ad here

Related Articles

See All Articles

You Might Also Like

Frontend best practices overview cover
Frontend / Fundamentals

Frontend Best Practices: An Overview

A friendly map of what good frontend really means — semantic HTML, clean CSS, unobtrusive JavaScript, responsive layouts, performance, and progressive enhancement, with pointers to go deeper.

Sep 20, 20268 min read
A Frontend Developer Roadmap cover with the path HTML to CSS to JavaScript
Frontend / Fundamentals

A Frontend Developer Roadmap: What to Learn, and in What Order

A calm, step-by-step learning path for beginners: HTML, CSS, JavaScript, developer tools, responsive and accessible design, a framework, TypeScript, and deployment, with the reasoning behind each step.

Sep 20, 202611 min read
Title card reading Styling and Interactivity over a dark blue ACY Partner background
Frontend / Fundamentals

How Styling and Interactivity Work

A beginner-friendly look at how CSS turns plain HTML into a designed layout, and how JavaScript adds behavior — the structure, style, behavior mental model for building web pages.

Sep 20, 20269 min read
Three front-end layers combining on one web page
Frontend / Fundamentals

How HTML, CSS, and JavaScript Work Together

A hands-on walkthrough for beginners: build one small button, give it structure with HTML, looks with CSS, and behavior with JavaScript, and watch the three layers cooperate on a real page.

Sep 20, 20268 min read
The Frontend Developer Toolkit cover with editor, browser, and CLI motifs
Frontend / Fundamentals

The Frontend Developer Toolkit

A friendly tour of the everyday tools a frontend developer relies on — code editor, browser and DevTools, terminal, version control, package managers, and a dev server — and what each one is actually for.

Sep 20, 202610 min read
Cover illustration for What Frontend Developers Do
Frontend / Fundamentals

What Frontend Developers Do: A Beginner's Guide to the Role

A clear, beginner-friendly look at what frontend developers actually do every day: turning designs into working interfaces, building reusable UI, and caring about responsiveness, accessibility, and speed.

Sep 20, 202610 min read