Mert Özen Further With Every Line
Frontend JavaScript Basics — #18

JavaScript Date Basics: Creating, Calculating and Formatting Dates

Mert Özen Nov 3, 2026 8 min 5 views
JavaScript Date Basics: Creating, Calculating and Formatting Dates

We see that a Date is really a timestamp, then learn how to create dates, the zero-based month trap, get/set methods, the difference between two dates and formatting with toLocaleDateString and Intl.DateTimeFormat.

Version note: The code in this post targets ES2026 and current browsers (Chrome, Edge, Firefox, Safari).

Predict First

We create a date and ask for its month and day:

const date = new Date(2026, 1, 30);
console.log(date.getMonth(), date.getDate());

What gets printed to the console? I won't give you a hint, but there are two separate traps in there. Make a guess before you keep reading. The answer is in the section where we talk about why months start at zero.

A Date Is Really a Number: Timestamps and the Epoch

The built-in way to work with dates and times in JavaScript is the Date object. The easiest way to understand it is this: a Date is an object that carries a single number. That number is the count of milliseconds since January 1, 1970, 00:00:00 UTC. That starting moment is called the epoch, and the number is called a timestamp.

Picture a giant stopwatch that was started in 1970 and has never stopped. Every Date is a reading taken from that stopwatch. The things people understand, like the day, month and hour, are calculated from that number. Paste this into the console and try it:

const now = new Date();
console.log(now);           // the current date and time
console.log(now.getTime()); // e.g. 1793700000000
console.log(Date.now());    // the same number without creating an object

console.log(new Date(0).toISOString()); // "1970-01-01T00:00:00.000Z"

Date.now() is especially handy for measuring how long something takes: grab it at the start and at the end of a task and look at the difference. The fact that a date is a number will also make calculating the difference between two dates very easy later in this post.

Ways to Create a Date

You can call new Date in a few different forms:

new Date();                         // right now
new Date(1793700000000);            // from a timestamp
new Date(2026, 0, 15);              // January 15, 2026, 00:00, local time
new Date(2026, 0, 15, 14, 30);      // January 15, 2026, 14:30, local time
new Date("2026-01-15T14:30:00");    // an ISO-format string, local time

The piece-by-piece form, where you pass the year, month, day, hour and minute, is the most predictable. Creating dates from strings is common too, but there are subtleties to watch for:

new Date("2026-01-15T00:00").getHours(); // 0: with a time, it's local time
new Date("2026-01-15").getHours();       // 3 in Türkiye: a date alone counts as UTC
new Date("15.01.2026");                  // non-standard format: don't rely on it
new Date("hello");                       // Invalid Date

The second line is the surprising one. According to the standard, a date-only string like "2026-01-15" is interpreted as UTC midnight, while "2026-01-15T00:00", which includes a time, is interpreted as local time. Türkiye is three hours ahead of UTC, so UTC midnight lands at 03:00 there. For a user further west, in the United States for example, the same string falls on the evening of the previous day.

With non-standard formats like "15.01.2026", the result is up to the browser; some give you Invalid Date, others produce a date you didn't expect. If you create dates from strings, use the ISO format (YYYY-MM-DDTHH:mm). To catch an invalid date, the Number.isNaN we saw earlier does the job: if Number.isNaN(date.getTime()) is true, the date is invalid.

Why Do Months Start at 0?

Did you notice we wrote new Date(2026, 0, 15) for January 15 above? In Date, months start at zero: January is 0, February is 1, December is 11. It's a decision inherited from Java's date class in the language's early days, and it has stayed that way because changing it would break the whole web. There's some logic to it, too: you can use the month number directly as an index into an array of names.

const months = ["January", "February", "March", "April", "May", "June",
  "July", "August", "September", "October", "November", "December"];

const d = new Date(2026, 4, 19);
console.log(months[d.getMonth()]); // "May"

What makes it confusing is that the rule isn't the same for every part. The month starts at zero, but the day of the month (getDate) starts at 1. The day of the week (getDay) starts at zero again, and zero is Sunday.

Now the opening question. new Date(2026, 1, 30) doesn't mean January 30, it means February 30. That's the first trap. The second: February 2026 has 28 days, and Date doesn't throw on values that overflow; it carries the extra into the next month. February 30 is two days after February 28, which is March 2. You see 2 2 in the console: 2 for March, 2 for the day of the month.

This overflow behavior is as useful as it is annoying. For example, to find the last day of a month, ask for day zero of the next month: new Date(2028, 2, 0).getDate() tells you how many days February 2028 has, which is 29.

get and set Methods

You read the parts of a date with the get methods:

const d = new Date(2026, 0, 15, 14, 30, 45);

d.getFullYear(); // 2026
d.getMonth();    // 0  (January)
d.getDate();     // 15 (day of the month)
d.getDay();      // 4  (Thursday; 0 = Sunday)
d.getHours();    // 14
d.getMinutes();  // 30
d.getSeconds();  // 45

Each of them has a set counterpart. The set methods change the date in place and handle the overflow for you. That turns calculations like "30 days from today" into a single line:

const due = new Date(2026, 0, 15);
due.setDate(due.getDate() + 30);
console.log(due.toLocaleDateString("tr-TR")); // "14.02.2026"

const copy = new Date(due); // an independent copy of the same moment
copy.setFullYear(2027);
console.log(due.getFullYear(), copy.getFullYear()); // 2026 2027

When we added 30 days, January ran out and the calculation moved into February on its own. The second part shows an important habit: Date is a mutable object. If you want to do a calculation without touching the original date, copy it first with new Date(oldDate). These methods also have UTC versions (such as getUTCHours and setUTCDate) that work in UTC instead of local time.

The Difference Between Two Dates

Since a date is a number at heart, you can subtract one date from another. The result is a number in milliseconds. Save and open this:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>New Year Countdown</title>
</head>
<body>
  <p id="today"></p>
  <p id="remaining"></p>
  <script>
    const today = new Date();
    const newYear = new Date(today.getFullYear() + 1, 0, 1);

    const diffMs = newYear - today;
    const oneDayMs = 1000 * 60 * 60 * 24;
    const days = Math.ceil(diffMs / oneDayMs);

    document.getElementById("today").textContent =
      "Today: " + today.toLocaleDateString("en-US", { dateStyle: "full" });
    document.getElementById("remaining").textContent = `${days} days until New Year.`;
  </script>
</body>
</html>

On the line newYear - today, the engine turns both dates into numbers and subtracts them; the coercion we saw earlier works in our favor here. Dividing the milliseconds by the milliseconds in a day gives you the number of days. Because today's time of day is part of it, the result has a fraction, so we round up with Math.ceil.

Comparison works the same way: date1 < date2 gives the result you expect. But comparing two dates with === returns false even if they point to the same moment. That's the object reference rule we saw earlier: two separate objects, two separate references. For equality, write date1.getTime() === date2.getTime().

A warning: in countries with daylight saving time, a day can last 23 or 25 hours, and a millisecond-based calculation can be off by an hour. Türkiye hasn't switched clocks since 2016, but your users may come from other countries. We'll cover this in detail in the Intermediate series.

Making Dates Readable: toLocaleDateString and Intl.DateTimeFormat

You don't need to write month names yourself to show a date to the user. The browser already knows the formatting rules for every language:

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

d.toLocaleDateString("tr-TR"); // "15.01.2026"
d.toLocaleDateString("tr-TR", {
  weekday: "long", year: "numeric", month: "long", day: "numeric"
}); // "15 Ocak 2026 Perşembe"
d.toLocaleTimeString("tr-TR"); // "14:30:00"
d.toLocaleDateString("en-US"); // "1/15/2026"
d.toLocaleDateString("de-DE"); // "15.1.2026"

The first argument is the language and region code, the second is an options object that says what to show and how. The same date is written month-first in the US, and with dots and no leading zeros in Germany. Leaving this to the browser instead of inventing your own format means less code and fewer bugs.

Behind the scenes, Intl.DateTimeFormat does this work. If you're showing lots of dates in the same format, creating one formatter and reusing it is more efficient:

const longFormat = new Intl.DateTimeFormat("tr-TR", { dateStyle: "long" });
console.log(longFormat.format(d)); // "15 Ocak 2026"

const fullFormat = new Intl.DateTimeFormat("tr-TR", { dateStyle: "full", timeStyle: "short" });
console.log(fullFormat.format(d)); // "15 Ocak 2026 Perşembe 14:30"

console.log(d.toISOString()); // in Türkiye: "2026-01-15T11:30:00.000Z"

There may be small differences in output between browsers; that's normal. If you're sending the date to a server or a JSON file rather than to a person, use toISOString(). It's always in UTC, and the Z at the end says so. The JSON.stringify we saw earlier turns dates into this format too. We'll see the other options of Intl.DateTimeFormat, and relative time phrases like "3 days ago," in the Intermediate series.

A Short Note on Time Zones

The number inside a Date points to the same moment everywhere in the world. Methods like getHours or toLocaleTimeString, however, interpret that moment in the time zone of the computer running the code. The same code shows 14:30 in Istanbul and 11:30 in London. To see the equivalent in another time zone, use the timeZone option:

const moment = new Date(2026, 0, 15, 14, 30); // assume it was created in Türkiye

const timeIn = (zone) =>
  new Intl.DateTimeFormat("tr-TR", { timeStyle: "short", timeZone: zone }).format(moment);

console.log(timeIn("Europe/Istanbul")); // "14:30"
console.log(timeIn("Europe/London"));   // "11:30"
console.log(timeIn("Asia/Tokyo"));      // "20:30"

console.log(Intl.DateTimeFormat().resolvedOptions().timeZone); // e.g. "Europe/Istanbul"

Over time, Date's shortcomings have become very clear: it's mutable, months start at zero, and working with time zones is a chore. The Temporal API was designed to fix these and is expected to enter the language with ES2027. Chrome and Firefox support it, but Safari doesn't yet. We'll look at Temporal in the Advanced series; until then, Date and Intl are the way that works in every browser.

Common Pitfalls

Using getYear

getYear() is an old, deprecated method; for 2026 it returns 126, because it gives you the years since 1900. Always use getFullYear().

Mixing up getDay and getDate

The names are similar, the meanings are different. getDate is the day of the month (1–31), getDay is the day of the week (0–6, Sunday is 0). If you read getDay expecting "the 15th," you get Thursday's number, 4.

Adding time to a date with +

Subtraction turns a date into a number, but + doesn't: new Date() + 1000 produces a string, with "1000" tacked onto the end of the date's text. If you want to add milliseconds, write new Date(date.getTime() + 1000).

Changing a shared date without noticing

If you pass a date to a function and call setDate inside it, the original date outside changes too; they're the same object. Copy it before you start calculating.

A Small Experiment

★☆☆ In the console, print today's date in the form "Tuesday, November 3, 2026" with "en-US". Then try the same with "tr-TR" and look at the difference.

★★☆ Predict before running, then try it:

const a = new Date(2026, 0, 31);
a.setMonth(1);
console.log(a.getMonth(), a.getDate());

★★★ Build an age calculator: take a birth date from a type="date" input and show on the page how old the user is. Hint: this input's value is a string like "1995-06-21", and if you pass it straight to new Date you fall into the UTC trap. Split the string with split("-") and use new Date(year, month - 1, day). If this year's birthday hasn't come yet, don't forget to count one year less.

In the last exercise we split a piece of text into parts based on a known pattern. But what if the pattern isn't that tidy? To search a text for a phone number, check whether an email is in the right format or find every date in one go, you need a more powerful tool. In the next post we'll meet regular expressions.