Skip to content

Basics

Dates, time zones, and Utilities.formatDate

A Date holds one instant in time. Use Utilities.formatDate(date, timeZone, pattern) and Session.getScriptTimeZone() to format it in the right time zone.

1 of 8

A Date is one instant in time

new Date() gives you the moment that line of code runs. A Date object holds a single instant, stored as a count of milliseconds since the start of 1970.

A Date does not carry a time zone with it. The same instant reads as 5:00 PM in New York and 10:00 PM in London. So the same Date can show two different times to two people, and why a date can look a day off.

new Date(2026, 0, 15) builds a particular day. Months are counted from 0, so 0 is January and 11 is December. Days of the month are counted from 1, as you would expect.

  • date.getTime() is the instant as a plain number of milliseconds.
  • date.getFullYear(), getMonth(), getDate(), getHours() read the parts in the project time zone set in Project Settings, which Session.getScriptTimeZone() returns.
  • getMonth() returns 0 for January, so add 1 before showing it to anyone.

2 of 8

The project time zone

Every Apps Script project has a time zone setting. Open Project Settings with the gear icon in the left sidebar of the editor and you will see it near the top. Fix it there if it is wrong, because a new project does not always guess your country correctly.

Session.getScriptTimeZone() returns that setting as text, for example "America/New_York". Pass it wherever a time zone is wanted, so your formatting follows the project setting instead of a value typed into the code.

A spreadsheet has its own time zone under File, then Settings. Make the two match. When they differ, a date the script writes can display an hour or a whole day away from what you expect.

3 of 8

Turn a Date into text with Utilities.formatDate

Utilities.formatDate(date, timeZone, pattern) turns a Date into a string. The pattern is made of letters, and each letter stands for one part of the date. Anything that is not a pattern letter, such as a comma or a dash, is kept as it is.

All three arguments are required, and the second argument must be a time zone string such as Session.getScriptTimeZone(). Passing the format pattern as the second argument or leaving timeZone undefined throws "Exception: Invalid argument: timeZone. Should be of type: String".

Capital letters matter. MM is the month and mm is the minutes. HH is the hour on a 24 hour clock and hh is the hour on a 12 hour clock.

  • 'yyyy-MM-dd' gives 2026-03-14. Good for file names, and it sorts correctly as text.
  • 'MMM d, yyyy' gives Mar 14, 2026.
  • 'EEEE' gives Saturday, the name of the day.
  • 'h:mm a' gives 4:05 PM.
  • 'yyyy-MM-dd HH:mm' gives 2026-03-14 16:05, on a 24 hour clock.
Format today three ways
function logTodayThreeWays() {
  const timeZone = Session.getScriptTimeZone();
  const now = new Date();

  Logger.log(Utilities.formatDate(now, timeZone, 'yyyy-MM-dd'));
  Logger.log(Utilities.formatDate(now, timeZone, 'EEEE, MMM d, yyyy'));
  Logger.log(Utilities.formatDate(now, timeZone, 'h:mm a'));
}

4 of 8

Dates that come out of a Sheet

When a cell holds a real Sheets date, getValues() and getValue() hand you a Date object, ready to use. Set the column’s number format to Date and enter a date rather than text. Also set the spreadsheet time zone under File, then Settings, and make it match the project time zone under Project Settings.

A cell that only looks like a date can hold a string instead. Do not pass that string to new Date(): date text can be ambiguous, and an ISO date-only string is interpreted as midnight UTC rather than in your spreadsheet’s calendar time zone. Reject text and fix the cell instead.

Use the spreadsheet time zone when formatting a date read from that spreadsheet. That preserves the calendar day the person entered, even if another project has a different time zone.

Read a real date from a cell
function logFirstDueDate() {
  const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = spreadsheet.getSheetByName('Tasks');
  if (!sheet) throw new Error('Add a Sheet tab named Tasks before running this function.');

  const dueDate = sheet.getRange('B2').getValue();
  if (!(dueDate instanceof Date) || isNaN(dueDate.getTime())) {
    throw new Error('Enter a real date in B2, not text, and format the cell as a date.');
  }

  Logger.log('Due ' + Utilities.formatDate(dueDate, spreadsheet.getSpreadsheetTimeZone(), 'yyyy-MM-dd'));
}

5 of 8

Add and subtract days

Two ways work well. setDate(getDate() + 7) moves a Date forward one week and rolls over into the next month or year for you. Or do the arithmetic in milliseconds: new Date(date.getTime() + 7 * 24 * 60 * 60 * 1000).

Change a copy, not the original. setDate() alters the Date object in place, so a Date you handed to something else moves too. new Date(other.getTime()) makes a fresh copy to work on.

The two ways differ on the two days a year when the clocks change, because a day of milliseconds is always 24 hours while a calendar day is sometimes 23 or 25. Prefer setDate() for anything a person reads.

A due date one week out, and the days remaining
function logDueDateAndDaysRemaining() {
  const timeZone = Session.getScriptTimeZone();
  const today = new Date();

  const dueDate = new Date(today.getTime());
  dueDate.setDate(dueDate.getDate() + 7);

  const millisecondsPerDay = 24 * 60 * 60 * 1000;
  const daysRemaining = Math.round((dueDate.getTime() - today.getTime()) / millisecondsPerDay);

  Logger.log('Due ' + Utilities.formatDate(dueDate, timeZone, 'EEEE, MMM d, yyyy'));
  Logger.log(daysRemaining + ' days remaining.');
}

6 of 8

The start and the end of a day

Calendar searches and date filters want a start and an end. Today as a Date is this instant, not the whole day, so a search from now until now finds nothing.

Build the two edges. Copy the date and call setHours(0, 0, 0, 0) for the first instant of the day, then copy again and call setHours(23, 59, 59, 999) for the last. setHours() sets hours, minutes, seconds, and milliseconds in one call.

For a single whole day on a calendar, CalendarApp.getDefaultCalendar().getEventsForDay(date) does this work for you. Build the edges yourself when the window is not one whole day.

The first and last instant of today
function logTodaysWindow() {
  const timeZone = Session.getScriptTimeZone();

  const startOfDay = new Date();
  startOfDay.setHours(0, 0, 0, 0);

  const endOfDay = new Date(startOfDay.getTime());
  endOfDay.setHours(23, 59, 59, 999);

  Logger.log('From ' + Utilities.formatDate(startOfDay, timeZone, 'yyyy-MM-dd HH:mm:ss'));
  Logger.log('To ' + Utilities.formatDate(endOfDay, timeZone, 'yyyy-MM-dd HH:mm:ss'));
}

7 of 8

Compare two dates

dateA === dateB is false even when both hold the same instant, because they are two separate objects and === asks whether they are the same object. Compare the numbers instead, with dateA.getTime() === dateB.getTime().

Less than and greater than do work straight on Date objects, so dueDate < today reads well and is correct. getTime() on both sides works for every comparison, including equality.

To compare days rather than instants, format both with "yyyy-MM-dd" and compare the two strings. Two times on the same afternoon then count as equal.

8 of 8

Write a date into a cell

setValue(new Date()) puts a real date in the cell. Sheets shows it using whatever format the cell has, and it sorts, filters, and works in date formulas like any date typed by hand.

Writing Utilities.formatDate(...) into the cell puts text there instead. It looks the same on screen, but it sorts alphabetically and no date formula will touch it.

So write the Date object into the sheet, and keep formatDate for text people read: an email body, a document, a log line, or a file name.

Stamp a cell with a real date
function stampCheckedTime() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Tasks');
  if (!sheet) throw new Error('Add a Sheet tab named Tasks before running this function.');

  sheet.getRange('D2').setValue(new Date());
  Logger.log('Wrote the current date and time into D2 of the Tasks tab.');
}

Remember

The short version

  • A Date is one instant, and the time zone decides what that instant looks like as text.
  • Format with Utilities.formatDate(date, Session.getScriptTimeZone(), pattern) so output follows the project setting.
  • Set a sheet date column’s number format and time zone, then use the real Date value it returns rather than parsing date text.
  • Copy a Date before calling setDate() or setHours(), because both change the object in place.
  • Write a Date into a cell to keep it a real date, and use formatted text only for people to read.

Keep going

Put it to work

All basics