// Sheets · Apps Script

Fix dates off by one day after the V8 migration.

Google Apps Script's V8 runtime parses ISO date strings as UTC midnight, not local time. Scripts that produced correct dates for years start writing the previous day into cells after migration. This page shows the exact fix.

I migrated my Apps Script project to V8 and now date cells show a day behind what my script sets, and I need to understand why and fix it without rewriting everything.

The script

copy · paste · trigger
fixV8Dates.gs
Apps Script
// V8-safe date construction — avoids UTC-midnight off-by-one
// Use parseLocalDate() anywhere you previously wrote new Date(isoString)

function parseLocalDate(isoString) {
  // Split the string; never pass an ISO-8601 date-only string to Date()
  var parts = isoString.split('-');
  var year  = parseInt(parts[0], 10);
  var month = parseInt(parts[1], 10);
  var day   = parseInt(parts[2], 10);
  return new Date(year, month - 1, day); // month is 0-indexed
}

function writeDatesToSheet() {
  var sheet = SpreadsheetApp.getActiveSheet();
  var isoDate = '2026-06-12';
  var safeDate = parseLocalDate(isoDate);
  sheet.getRange('A1').setValue(safeDate);
}

Need a variant? Gnaw writes a custom version from one sentence — fields, triggers, edge cases handled.

Walkthrough

Why V8 changed the behavior

The Rhino runtime that Apps Script used before V8 was not fully ES5-compliant in how it parsed date strings. When you wrote new Date('2026-06-12'), Rhino treated that string as local time, so in a New York-timezone script, you got midnight on June 12 Eastern — which is what ended up in the cell.

V8 follows the ECMAScript spec correctly: date-only strings in ISO-8601 format (YYYY-MM-DD, no time component, no Z) are parsed as UTC midnight. For any script running in a timezone behind UTC — UTC-1 through UTC-12 — that UTC midnight falls on the previous calendar day locally. The cell shows June 11. The script has not broken, it is now spec-compliant, which is worse.

The frustrating part is that the bug is silent. No error is thrown. Values look almost right. You only catch it when a date-keyed lookup misses, or someone notices the row is off by a day. Scripts that ran correctly for years on Rhino start silently writing wrong data.

The two reliable fixes

Fix 1, and the one I keep in a shared utils file for every project: construct dates with the multi-argument Date constructor — new Date(year, month - 1, day). This form never touches UTC. The month is 0-indexed (January = 0), which is its own classic source of bugs, but at least it is consistent and predictable across both runtimes.

Fix 2 is Utilities.parseDate(dateString, timeZone, format). This is the Apps Script-native approach and it makes the timezone explicit: Utilities.parseDate('2026-06-12', Session.getScriptTimeZone(), 'yyyy-MM-dd'). The advantage is that you are reading the timezone from the script's own settings, so if someone deploys the same script in a different region, it adjusts automatically. The disadvantage is that it is slower and more verbose for tight loops over large ranges.

For bulk processing — reading a column of 500 ISO date strings from a sheet and converting them all — use the split-and-construct approach in a loop. Utilities.parseDate has per-call overhead that compounds.

Spotting affected code in an existing script

Search for new Date( in your script. Any call where the argument is a string variable or a string literal in YYYY-MM-DD format is a candidate. Calls that pass a full datetime string with a time component (like '2026-06-12T10:00:00') are also affected in some timezones depending on whether the Z suffix is present, but the date-only case is the most common source of the off-by-one.

Calls where you pass separate numeric arguments — new Date(2026, 5, 12) — are already safe. Calls to new Date() with no arguments (current time) are also unaffected.

The first time I hit this, the bug only appeared in reports generated after 5 PM Pacific, because that is when Pacific time crosses into the previous UTC day. Scripts run during business hours tested fine. That intermittent quality made it genuinely hard to track down before understanding the root cause.

Want a custom version?

Describe your sheet and the rule you want. Gnaw writes the Apps Script — fields, triggers, edge cases — in one shot.

FAQ

4 questions
Does this affect all timezones or only ones west of UTC?
Only timezones behind UTC (UTC-1 through UTC-12). In UTC+1 or later, parsing '2026-06-12' as UTC midnight still falls on June 12 locally, so those scripts never surfaced the bug. If your script runs in a UTC+ timezone and you add a time component without a Z suffix, you can hit a different but related drift in the opposite direction.
Does Utilities.formatDate have the same problem when writing dates back to strings?
No. Utilities.formatDate(date, timeZone, format) takes an explicit timezone argument and formats relative to it. The bug is on the parsing side — constructing a Date object from a string. Once you have a correctly-constructed Date object in memory, formatDate behaves consistently.
My script reads dates from a Sheets cell using getValue(). Is that also affected?
No. When Sheets returns a date cell value via getValue() or getValues(), Apps Script gives you a JavaScript Date object already anchored to the spreadsheet's timezone — not a string. The off-by-one only happens when you parse a raw ISO string yourself using new Date(isoString).
Can I just roll back to the Rhino runtime to fix this?
Technically yes — removing the V8 runtime flag in appsscript.json reverts to Rhino. But Google has signaled Rhino is deprecated and will eventually be removed. Rolling back trades a fixable one-day bug for a runtime you cannot depend on long-term. Fix the date parsing.