JavaScriptの日付が1日前になる原因|日本(JST)では toISOString が犯人
JavaScript で日付を扱っていて、「画面では10月14日なのに、保存された値は10月13日になっている」という現象に出会ったことはないでしょうか。結論から言うと、日本(JST)で起きる「1日前」のほとんどは、new Date("2026-10-14") ではなく toISOString() が原因です。
検索すると「日付だけの文字列は UTC として解釈されるので1日前になる」という説明が多く出てきますが、これは UTC より西側(米国など)の話です。日本は UTC より東(+9時間)なので、同じ理屈では起きません。この記事では、実際に Node.js で JST・UTC・米国東部の3つのタイムゾーンを切り替えて動かした結果をもとに、原因と直し方を整理します。
結論:まず自分の症状を切り分ける
先に、症状と原因の対応表を示します。
| 症状 | 日本(JST)での主な原因 | 対処の方向 |
|---|---|---|
| 日付を文字列に直したら1日前になる | toISOString().slice(0, 10) が UTC の日付を返している | ローカルの年月日を自分で組み立てる、または Intl でタイムゾーンを指定する |
| 開発機では合うのに、本番サーバーでずれる | 本番が UTC 設定で、ローカル時刻の解釈が変わっている | 日付だけの値は「文字列」で持ち、Date に変換しない |
| 日付だけ渡したのに時刻が9時になる | 日付のみの ISO 文字列は UTC 0時として解釈される | 仕様どおり。表示や計算でローカル暦日として使うなら生成方法を変える |
| 存在しない日付が翌月になる | new Date(2026, 1, 30) は繰り上がる | 入力値の検証を別に行う |
以降、この表の順に実測結果を見ていきます。
実測:同じコードでもタイムゾーンで結果が変わる
次のコードを、実行環境のタイムゾーンだけ変えて動かしました。Node.js v20 で、環境変数 TZ を切り替えています。
const a = new Date("2026-10-14"); // 日付のみ
const c = new Date(2026, 9, 14); // ローカルの10月14日0時(月は0始まり)
console.log(a.getDate()); // ローカルの「日」
console.log(c.toISOString().slice(0, 10));
結果は次のとおりです(私が手元で実行して確認した値です)。
| 式 | JST(UTC+9) | UTC | 米国東部(夏時間、UTC-4) |
|---|---|---|---|
new Date("2026-10-14").getDate() | 14 | 14 | 13 |
new Date("2026-10-14").getHours() | 9 | 0 | 20 |
new Date(2026, 9, 14).toISOString().slice(0, 10) | 2026-10-13 | 2026-10-14 | 2026-10-14 |
new Date("2026-10-14").toLocaleDateString("ja-JP") | 2026/10/14 | 2026/10/14 | 2026/10/13 |
ポイントは、「ずれる側」がタイムゾーンによって逆になることです。
- 米国など UTC より西:日付のみの文字列 → ローカル表示で前日になる(よく紹介されるパターン)
- 日本など UTC より東:ローカルの0時 →
toISOString()で前日になる(日本で踏みやすいパターン)
日本向けのサービスを日本の環境でテストしている限り、new Date("2026-10-14") は「14日の9時」になり、日付としては問題が見えません。一方で toISOString() を使った瞬間に1日前になります。
原因:toISOString は常に UTC で返す
toISOString() は、結果の末尾が Z であることからも分かるとおり、常に UTC 基準の文字列を返します。JST の10月14日0時は、UTC では10月13日15時です。
new Date(2026, 9, 14).toISOString();
// "2026-10-13T15:00:00.000Z" ← JST で実行した結果
ここから slice(0, 10) で先頭10文字を取ると 2026-10-13 になります。日付を YYYY-MM-DD にしたい場面でこの書き方が広く使われているため、日本では「0時付近の日付が1日前になる」バグとして頻繁に現れます。
午前9時までが「危険な時間帯」
この現象は時刻にも依存します。JST の「その日の0時〜8時59分」に生成した日時だけが前日になります。
new Date("2026-10-14T08:30:00+09:00").toISOString().slice(0, 10); // "2026-10-13"
new Date("2026-10-14T09:30:00+09:00").toISOString().slice(0, 10); // "2026-10-14"
つまり、昼間にテストすると再現せず、早朝や深夜の処理でだけずれることになります。「たまに1日ずれる」「午前中の登録分だけ日付が違う」といった報告は、この典型例と考えられます。
直し方:ローカルの暦日を安全に取り出す
方法1:年月日を自分で組み立てる
ブラウザや Node の実行環境のタイムゾーンで「今日の日付」を取りたいだけなら、getFullYear() などのローカル用メソッドを使います。
const ymdLocal = (d) =>
`${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
ymdLocal(new Date(2026, 9, 14)); // "2026-10-14"(JST・UTC・米国東部のいずれでも同じ)
3環境で実行して、いずれも 2026-10-14 になることを確認しました。
方法2:Intl でタイムゾーンを明示する
サーバーのタイムゾーンに左右されず「日本の日付」を取りたい場合は、Intl.DateTimeFormat に timeZone を渡します。
const ymdTokyo = (d) =>
new Intl.DateTimeFormat("sv-SE", { timeZone: "Asia/Tokyo" }).format(d);
ymdTokyo(new Date("2026-10-14T00:30:00+09:00")); // "2026-10-14"
ロケールに sv-SE(スウェーデン語)を指定すると、YYYY-MM-DD 形式で出力される点を利用しています。この出力が実質的に YYYY-MM-DD になることは、手元の Node v20 で確認しました。他の実行環境で同じ形式になるかは確認していないため、本番で使う前に動かして確かめてください。en-CA でも同様の形式になる環境がありますが、こちらも環境依存と考えられます。
方法3:日付だけの値は Date に変換しない
誕生日・予約日・締切日のように「その日」だけを表す値は、そもそも YYYY-MM-DD の文字列のまま持つのが最も安全です。<input type="date"> の値もこの形式の文字列で返ります。Date に変換するのは、日数の加減算など計算が必要なときだけにします。
計算が必要なときは、文字列を分解してローカルの Date を作ります。
const parseYmd = (s) => {
const [y, m, d] = s.split("-").map(Number);
return new Date(y, m - 1, d); // ローカルの0時
};
const x = parseYmd("2026-10-31");
x.setDate(x.getDate() + 1);
ymdLocal(x); // "2026-11-01"
この「パースはローカル、書式化もローカル」の組み合わせであれば、3環境で同じ結果になることを確認しました。
他所ではあまり書かれていない落とし穴
落とし穴1:開発機(JST)と本番(UTC)で結果が変わる
もっとも実害が出やすいのがこれです。手元の PC は JST、デプロイ先のサーバーやコンテナ、CI は UTC というのはよくある構成です(多くのクラウド環境は UTC が既定と考えられますが、お使いの環境では確認してください)。
先ほどの実測表で、toISOString().slice(0, 10) は JST では 2026-10-13、UTC では 2026-10-14 でした。つまり、開発機で「バグがある」と見えていたコードが、本番では偶然正しく動くことがあります。
これを見て「本番では問題ないから放置」とするのは危険です。のちにサーバーのタイムゾーン設定を変えた時、あるいは日本時間で処理するバッチを別のリージョンに移した時に、突然ずれ始めます。どの環境でも同じ結果になる書き方にしておくのが重要です。
落とし穴2:UTCで動くサーバーで「今日の日付」を取ると、日本の午前9時までは前日になる
サーバーが UTC のまま new Date() から日付を作ると、日本時間の0時〜8時59分は、サーバー上ではまだ前日です。日次の集計や「本日締切」の判定を日本時間の暦日で行いたい場合、サーバーのローカル日付を使うと、午前中だけ前日扱いになります。この場合は方法2のように timeZone: "Asia/Tokyo" を明示するのが確実です。
落とし穴3:new Date(年, 月, 日) は存在しない日付を繰り上げる
月は0始まりなので 9 が10月です。さらに、存在しない日付はエラーにならず繰り上がります。
ymdLocal(new Date(2026, 1, 30)); // "2026-03-02"
「2月30日」が3月2日になります。入力フォームの値をそのまま渡すと、不正な日付が通ってしまいます。いっぽう、new Date("2026-13-01") のように書式として不正な文字列は Invalid Date になります。繰り上がりは「数値を直接渡した場合」、無効判定は「文字列を渡した場合」と、挙動が分かれます。入力検証は、Date に渡す前に別途行ってください。
落とし穴4:"2026-1-5" のような非標準の書式
ゼロ埋めのない "2026-1-5" は、仕様で定められた ISO 形式ではありません。手元の Node v20 ではローカル時刻の0時として解釈されましたが、ブラウザや実行環境によって扱いが異なる可能性があり、この点は検証していません。日付文字列は、ゼロ埋めした YYYY-MM-DD を使うのが安全です。
日付と時刻の文字列、どれがどう解釈されるか
整理のために、入力形式ごとの解釈を示します。JST で実測した結果と、仕様として一般に説明されている内容をあわせた表です。
| 入力 | 解釈 | JSTでの結果 |
|---|---|---|
"2026-10-14" | UTC の0時 | 2026-10-14 09:00(JST) |
"2026-10-14T00:00:00" | ローカル時刻の0時 | toISOString() は 2026-10-13T15:00:00.000Z |
"2026-10-14T00:00:00+09:00" | 時差を明示 | toISOString() は 2026-10-13T15:00:00.000Z |
"2026/10/14" | 非標準。実装依存(Node ではローカル0時) | toISOString() は 2026-10-13T15:00:00.000Z |
"2026-10-14" と "2026-10-14T00:00:00" は、見た目が似ていても 9時間(JST の場合)違う瞬間を指します。実際に Date.parse の差を取ると、32,400,000 ミリ秒(9時間)でした。この違いを知らないまま、片方で保存し、もう片方で比較すると、境界の日付でずれます。
原因を確かめたいときの手順
「どこでずれたのか」が分からないときは、次の順に確認します。
- 問題の値を
console.log(d.toString())とconsole.log(d.toISOString())の両方で出す - 差が9時間(JST の場合)あれば、タイムゾーンの解釈が原因と判断できる
- その値が、サーバー・ブラウザ・DB のどこで文字列化されたかを追う
- 実行環境のタイムゾーンを確認する(
Intl.DateTimeFormat().resolvedOptions().timeZone)
console.log(Intl.DateTimeFormat().resolvedOptions().timeZone);
保存されている値がミリ秒や秒の数値であれば、Unixタイムスタンプ変換ツールで UTC と JST を切り替えて確認できます。同じ数値が UTC と JST でどう違う日時になるかを見比べると、9時間のずれが原因かどうかを切り分けられます。タイムスタンプの仕組み自体は Unixタイムスタンプとは にまとめています。
なお、定期実行の時刻も、サーバーのタイムゾーンに従って動きます。日付の境界で処理を動かす場合は cron式の書き方 もあわせて確認してください。
よくある質問
Date を使わず、ライブラリに任せたほうがよいですか
日付計算が多い場合は、日付ライブラリを使うのも一つの選択です。ただし、ライブラリを入れても「日付だけの値を瞬間(UTC)として扱うのか、暦日として扱うのか」という設計の問題は残ります。この記事の方針(日付だけの値は文字列で持つ)は、どのライブラリを使う場合にも有効と考えられます。
新しい日付 API(Temporal)で解決しますか
日付だけの値を表す型が別に用意される方向で標準化が進んでいると言われていますが、利用できるかどうかは実行環境によって異なり、今回は検証していません。使う場合は、対象のブラウザや Node のバージョンでの対応状況を確認してください。
DBの DATE 型や TIMESTAMP 型はどうなりますか
DB 製品や接続ドライバの設定によって、タイムゾーンの扱いが異なります。今回は検証していないため、利用中の DB とドライバのドキュメントを確認してください。
まとめ
- 日本(JST)で日付が1日前になる主因は、
toISOString()が UTC で返すこと。new Date("YYYY-MM-DD")は、米国など UTC より西で起きる話 - JST の0時〜8時59分の日時だけが前日になるため、昼間のテストでは再現しにくい
- 直し方は、ローカル用メソッドで年月日を組み立てるか、
Intl.DateTimeFormatでtimeZoneを明示する - 誕生日・予約日のような日付だけの値は、
YYYY-MM-DDの文字列のまま持つのが最も安全 - 開発機が JST、本番が UTC だと結果が変わる。どの環境でも同じ結果になる書き方に統一する
なお、本記事の実測値は Node.js v20 での結果です。ブラウザでの挙動は同じ仕様に従うと考えられますが、今回は検証していません。環境や仕様の細部は変わることがあるため、重要な処理には、お使いの環境でのテストを必ず用意してください。