JavaScript SyntaxError: Unexpected token ‘<‘ in JSONの原因と解決方法【fetchでHTMLが返る罠】

SyntaxError: Unexpected token ‘<‘ in JSONとは?ざっくりと3行で
  • SyntaxError: Unexpected token ‘<‘ in JSONは、JSONを期待した処理にHTMLなどの別データが渡ったときに発生するエラーだ。
  • 実務でいちばん多いのはAPIがJSONではなくHTMLのエラーページを返しているケースで、res.json()が先頭の<で詰まっている。
  • まずはレスポンスの中身をres.text()で覗き、res.okとContent-Typeを確認してからパースするのが直し方の基本になる。
fetchのres.json()でSyntaxError: Unexpected token '<'が出て、APIがHTMLを返していたと気づき解決するまでの4コマ
①コンソールが赤くなりUnexpected tokenのエラーが出る。②Network欄を見るとAPIがHTMLを返している。③res.okを先に確認してエラーが消える。④原因はURLの打ち間違いだった。

この4コマは、fetchで受け取ったレスポンスを中身を確認せずにres.json()へ渡したという、現場で最も頻発する失敗を再現したケーススタディです。1コマ目でコンソールに出る<は、本来JSONの先頭にあるはずの{ではなくHTMLの開始タグを読んでしまった動かぬ証拠になります。パーサは1文字目でつまずくため、位置はposition 0と表示されます。

2コマ目でDevToolsのNetworkパネルを開くと、レスポンス本文が<!DOCTYPE html>で始まっていることがわかります。API連携ではこの一手間を飛ばすと、原因の切り分けに何時間も溶かしがちです。このエラーを本番で放置すると、画面が真っ白のまま利用者に何も表示されない障害につながります。3コマ目のようにres.okとContent-Typeを先に確かめる防御を1行足すだけで、原因不明のクラッシュを親切なエラー表示に変えられます。オチが示すとおり、根本原因がURLの打ち間違いという小さなミスであることも珍しくありません。

  1. SyntaxError: Unexpected token ‘<‘ in JSONの基本情報
  2. なぜSyntaxError: Unexpected token ‘<‘ in JSONは発生するのか?
    1. パターン1:APIが404/500のHTMLエラーページを返している
    2. パターン2:URLミスでSPAのindex.htmlが返ってくる
    3. パターン3:POST先が500エラーでフレームワークのデバッグ画面を返す
  3. SyntaxError: Unexpected token ‘<‘ in JSONは環境によって表示が変わる?
  4. ReactやNext.jsでSyntaxError: Unexpected token ‘<‘ in JSONが出るのはなぜ?
    1. Reactでの発生パターン
    2. Next.jsでの発生パターン
  5. エラーメッセージのバリエーション別に見る対処法は?
    1. Unexpected token ‘<‘, ” 先頭が<!DOCTYPEのときは、ほぼ確実にHTMLページを受け取っています。APIのURL・メソッド・認証の3点を疑い、ブラウザで直接そのURLを開いて何が返るか確認するのが最短です。 // まず生の中身を確認するデバッグ const res = await fetch('/api/users'); const raw = await res.text(); console.log('status:', res.status); console.log('body先頭:', raw.slice(0, 120)); // <!DOCTYPE html> ならHTML確定
    2. Unexpected token < in JSON at position 0 と出る場合
  6. SyntaxError: Unexpected token ‘<‘ in JSONはどうデバッグすればいい?
  7. SyntaxError: Unexpected token ‘<‘ in JSONを未然に防ぐには?
  8. res.json()の内部では何が起きているのか?
  9. よくある質問
  10. この記事と一緒に知っておきたいエラー解決
  11. 【出典】参考URL

SyntaxError: Unexpected token ‘<‘ in JSONの基本情報

SyntaxError: Unexpected token ‘<‘ in JSONは、JSON.parse()Response.json()がJSON文字列を解析する途中で、JSONとして不正な文字(ここでは先頭の<)に出会ったときに投げられる構文エラーです。

項目内容
エラーメッセージSyntaxError: Unexpected token ‘<‘, “<!DOCTYPE “… is not valid JSON(旧表記:Unexpected token < in JSON at position 0)
発生する言語・環境JavaScript(ブラウザのfetch/XMLHttpRequest、Node.js 18/20/22など)
エラーの意味JSONを解析中に予期しない記号<が現れた、という意味。先頭がHTMLタグであることを示す
主な原因APIがJSONではなくHTML(404/500ページやindex.html)を返し、それをres.json()でパースした
解決の基本方針パース前にres.okとContent-Typeを確認し、まずres.text()で実際の中身を見る
ITKAGYO運営者デプロイ太郎のアイコン画像

コード自体は正しく見えるのに出るので、初見だと焦りますよね。犯人はコードではなくサーバーが返した中身のことが多いです。

なぜSyntaxError: Unexpected token ‘<‘ in JSONは発生するのか?

意外に思われますが、このエラーはJSONの書き方を間違えたときより、通信の受け取り方を間違えたときに出る割合のほうが高く、実務ではAPIのURLミスやサーバー側のエラーページが原因の大半を占めます。

SyntaxError: Unexpected token ‘<‘ in JSONの正体は、パーサがJSONの1文字目に{[を期待しているのに、HTMLの<を読んでしまうことです。ここでは発生頻度の高い順に、代表的な3つのパターンを見ていきます。

パターン1:APIが404/500のHTMLエラーページを返している

最も多いのが、リクエスト先のパスが存在せず、サーバーがJSONではなくHTMLのエラーページを返すケースです。res.okを確認せずにパースすると先頭の<で失敗します。

// Node.js 20 / ブラウザfetch
async function loadUsers() {
  const res = await fetch('/api/users'); // 実際は404でHTMLページが返る
  const data = await res.json();         // ← ここで SyntaxError が発生
  return data;
}
// レスポンス本文の先頭が <!DOCTYPE html> のため 1文字目 '<' で失敗する

ポイントは、fetchはHTTPステータスが404や500でも例外を投げず、res.okfalseのまま処理を続ける点です。ステータスを確認せずにJSONと決めつけていることが根本原因になります。パース前にres.okで成否を判定しましょう。

async function loadUsers() {
  const res = await fetch('/api/users');
  // 200番台でなければJSONと決めつけない
  if (!res.ok) {
    const body = await res.text(); // 実際の中身を確認できる
    throw new Error(`APIエラー ${res.status}: ${body.slice(0, 100)}`);
  }
  return res.json(); // ここに来る時点で正常レスポンスと分かる
}

パターン2:URLミスでSPAのindex.htmlが返ってくる

開発サーバーやSPAでは、存在しないパスにアクセスするとステータス200のままindex.htmlが返る設定が多く、この場合はres.oktrueでもHTMLが届きます。

// 先頭スラッシュ抜けの相対パス。SPAのindex.htmlに吸収される
fetch('api/todos')
  .then((res) => res.json()) // 200 OK だが本文はHTML → SyntaxError
  .then((data) => render(data));

ステータスが正常でも安心できないため、res.okだけでなくContent-Typeがapplication/jsonかどうかを確かめるのが確実です。次のように中身の種類でガードします。

fetch('/api/todos') // 絶対パスに修正
  .then(async (res) => {
    const ct = res.headers.get('content-type') ?? '';
    if (!ct.includes('application/json')) {
      const text = await res.text();
      throw new Error('JSONではありません: ' + text.slice(0, 80));
    }
    return res.json();
  })
  .then((data) => render(data));

パターン3:POST先が500エラーでフレームワークのデバッグ画面を返す

データ送信時にサーバー側で例外が起き、LaravelやRailsなどがHTMLのエラー画面を返すと、その本文をパースして同じエラーになります。

const res = await fetch('/api/order', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify(payload),
});
const json = await res.json(); // サーバ側500でHTMLのエラー画面 → Unexpected token '<'

この場合もやることは同じで、res.okで分岐し、失敗時はres.text()で中身をログに残します。失敗レスポンスの本文を握りつぶさないことが、サーバー側のスタックトレースへ最短でたどり着くコツです。

const res = await fetch('/api/order', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify(payload),
});
if (!res.ok) {
  console.error('注文API失敗', res.status, await res.text());
  throw new Error(`注文の送信に失敗しました(${res.status})`);
}
const json = await res.json();
SyntaxError: Unexpected token '<'の原因を解説するデプロイ太郎のアイコン画像

パターン1と2、現場だと本当によく見ます。まずは中身を目で見る、これだけで解決が一気に近づきますよ。

SyntaxError: Unexpected token ‘<‘ in JSONは環境によって表示が変わる?

SyntaxError: Unexpected token ‘<‘ in JSONは、同じ原因でも実行するJavaScriptエンジンによって文言が違います。エラーで検索するときは自分の環境の表記に合わせると情報を見つけやすくなります。

実行環境表示されるエラーメッセージ
Google Chrome 111以降 / Node.js 20以降(V8)SyntaxError: Unexpected token ‘<‘, “<!DOCTYPE “… is not valid JSON
Google Chrome 110以前 / 旧V8SyntaxError: Unexpected token < in JSON at position 0
Firefox(SpiderMonkey)SyntaxError: JSON.parse: unexpected character at line 1 column 1 of the JSON data

表のとおり、比較的新しいV8ではis not valid JSONと失敗した文字の周辺まで出るようになり、位置の代わりに実際の中身が示されます。Safari(JavaScriptCore)はさらに書き方が異なり、メッセージがJSON Parse error:という接頭辞から始まる形式になります。どのエンジンでも意味は同じで、JSONの途中でHTMLの記号に出会ったという点は共通です。

ReactやNext.jsでSyntaxError: Unexpected token ‘<‘ in JSONが出るのはなぜ?

Reactでの発生パターン

ReactではuseEffect内でデータを取得する実装が定番ですが、開発サーバーのプロキシ設定が抜けているとAPIリクエストがindex.htmlに吸収され、HTMLが返ってきます。useStateにセットする前のres.json()で失敗し、コンポーネントが描画されません。create-react-appViteproxy設定漏れが典型的なきっかけになります。

// 発生する例
useEffect(() => {
  fetch('/api/user')
    .then((res) => res.json()) // プロキシ未設定でHTMLが返る → SyntaxError
    .then(setUser);
}, []);

// 修正例:中身を確認してからパースする
useEffect(() => {
  fetch('/api/user')
    .then(async (res) => {
      if (!res.ok) throw new Error(`APIエラー ${res.status}`);
      const ct = res.headers.get('content-type') ?? '';
      if (!ct.includes('application/json')) {
        throw new Error('JSONではないレスポンスです');
      }
      return res.json();
    })
    .then(setUser)
    .catch((e) => setError(e.message)); // 画面に安全に反映
}, []);
Viteならvite.config.jsserver.proxy、CRAならpackage.jsonproxyを設定し、APIが確実にバックエンドへ届くようにしましょう。

Next.jsでの発生パターン

Next.jsではApp RouterのRoute HandlerやPages APIのパスがずれていると、リクエストがNextの404ページ(HTML)に落ちて同じエラーになります。rewritesbasePathの設定ミス、末尾スラッシュの有無も原因になりやすい箇所です。

// app/page.tsx などクライアント側
// 発生する例:パスが実在せずNextの404 HTMLが返る
const res = await fetch('/api/posts'); // 実体は /api/post
const data = await res.json();         // Unexpected token '<'

// 修正例:ステータスとcontent-typeを確認
const res = await fetch('/api/post');
if (!res.ok) throw new Error(`APIエラー ${res.status}`);
if (!res.headers.get('content-type')?.includes('application/json')) {
  throw new Error('JSONではありません: ' + (await res.text()).slice(0, 80));
}
const data = await res.json();
Route Handlerのファイルはapp/api/post/route.tsのように配置し、フェッチ側のパスと1文字単位で一致しているかを確認しましょう。

エラーメッセージのバリエーション別に見る対処法は?

Unexpected token ‘<‘, ” 先頭が<!DOCTYPEのときは、ほぼ確実にHTMLページを受け取っています。APIのURL・メソッド・認証の3点を疑い、ブラウザで直接そのURLを開いて何が返るか確認するのが最短です。 // まず生の中身を確認するデバッグ const res = await fetch('/api/users'); const raw = await res.text(); console.log('status:', res.status); console.log('body先頭:', raw.slice(0, 120)); // <!DOCTYPE html> ならHTML確定

Unexpected token < in JSON at position 0 と出る場合

position 0は1文字目でつまずいたという意味で、レスポンス全体がHTMLである可能性が高いサインです。positionが0以外のときは、JSONの途中にHTMLやログ文字列が混ざっているケースを疑います。

// position が 0 以外なら、先頭は正常でも途中で壊れている
// 例:JSONの前後にPHPの警告HTMLが出力されている等
// → サーバー側の出力を修正し、余計な出力を止める

SyntaxError: Unexpected token ‘<‘ in JSONはどうデバッグすればいい?

SyntaxError: Unexpected token ‘<‘ in JSONのデバッグは、パースの前段階に戻って実際に届いたデータを確認するのが鉄則です。ブラウザのDevToolsではNetworkタブで該当リクエストを選び、Responseプレビューで本文がHTMLかJSONかを目視できます。コードからはパースをやめてres.text()に切り替えると中身がそのまま見えます。

async function debugFetch(url) {
  const res = await fetch(url);
  const ct = res.headers.get('content-type');
  const raw = await res.text(); // json() ではなく text() で受ける
  console.log({ status: res.status, ok: res.ok, contentType: ct });
  console.log('本文の先頭:', raw.slice(0, 200));
  // ここで <!DOCTYPE html> が見えたら、URLかサーバー設定を疑う
}
SyntaxError: Unexpected token '<'のデバッグ手順を案内するデプロイ太郎のアイコン画像

迷ったらまずres.text()。中身さえ見えれば、あとはURLかサーバーかの二択まで一気に絞れます。

SyntaxError: Unexpected token ‘<‘ in JSONを未然に防ぐには?

SyntaxError: Unexpected token ‘<‘ in JSONの予防は、JSONを扱う処理を1つのヘルパー関数に集約し、res.okとContent-Typeのチェックを必ず通すことで実現できます。個々のfetchにチェックを書き忘れる事故がなくなります。

// 共通のJSON取得ヘルパー
async function fetchJson(url, options) {
  const res = await fetch(url, options);
  const ct = res.headers.get('content-type') ?? '';
  if (!res.ok || !ct.includes('application/json')) {
    const text = await res.text();
    throw new Error(`不正なレスポンス(${res.status}): ${text.slice(0, 80)}`);
  }
  return res.json();
}

// 呼び出し側はすっきり、かつ安全
const users = await fetchJson('/api/users');
Axiosは非2xxで自動的に例外を投げ、既定でJSONを扱うため、チェック漏れを仕組みで防ぎたいならAxiosの導入も有効な選択肢です。

res.json()の内部では何が起きているのか?

Response.json()は、レスポンス本文をまずテキストとして読み取り、その文字列をJSON.parse()に相当する処理へ渡します。つまりres.json()のエラーは、実質的にJSON.parse('<!DOCTYPE html>...')を実行したのと同じ状況です。JSONの文法では値の開始は{[、あるいは文字列・数値でなければならず、<はどの規則にも当てはまりません。だからパーサは1文字目で解析を打ち切り、先頭がHTMLタグであることを構文エラーとして報告します。この仕組みを理解しておくと、JSONの読み込みの前にres.text()で中身を確認するという発想に自然につながります。データ形式そのものについてはJSONとは?Web APIの標準データ形式を理解する最重要フォーマットを解説もあわせて読むと理解が深まります。

SyntaxError: Unexpected token '<'の予防をまとめるデプロイ太郎のアイコン画像

ヘルパー1個で今後の事故をまとめて防げます。落ち着いて一つずつ確認していきましょう!

よくある質問

Q
ローカルでは動くのに本番環境だけこのエラーが出るのはなぜですか?
A

本番のリバースプロキシやCDNがAPIパスを正しく転送できていないことが多いです。ローカルでは開発サーバーのプロキシがAPIへ届けていても、本番では未設定のパスがフロントのindex.htmlや404ページに落ち、HTMLが返ってこのエラーになります。本番のNetworkタブで実際のレスポンスを確認してください。

Q
res.okがtrueなのにUnexpected token ‘<‘が出るのはどういう状況ですか?
A

ステータス200のままHTMLが返っている状況です。SPAのフォールバック設定では、存在しないパスでも200でindex.htmlを返すため、res.okは成功を示します。ステータスだけでなくContent-Typeがapplication/jsonかどうかも確認すると、この取りこぼしを防げます。

Q
このエラーをLinterやツールで事前に防ぐ方法はありますか?
A

Linter単体で完全には防げませんが、JSON取得を共通ヘルパーに集約し、ESLintのno-restricted-syntaxで生のres.json()直呼びを警告する運用が効果的です。加えて非2xxで自動的に例外を投げるAxiosを使えば、チェック忘れそのものを仕組みで減らせます。

Q
利用者に画面が真っ白になるのを防ぐエラーハンドリングはどう書きますか?
A

取得処理をtry/catchで囲み、失敗時は状態変数にエラーを保存して代替UIを表示します。パースの失敗をそのまま握りつぶすと画面が真っ白になるため、読み込み中・成功・失敗の3状態を持たせ、失敗時は再試行ボタンなどを出すと利用者が操作を継続できます。

Q
SyntaxError: Unexpected token ‘<‘とTypeError: Failed to fetchの違いは何ですか?
A

発生する段階が異なります。TypeError: Failed to fetchは通信自体が成立しない段階(CORS拒否・ネットワーク不通など)で起き、レスポンスを受け取れません。一方SyntaxError: Unexpected token ‘<‘は通信は成功してレスポンスを受け取れたものの、その中身がJSONではなくHTMLだったために解析で失敗する点が違います。

この記事と一緒に知っておきたいエラー解決

関連エラー この記事との関連
JavaScript SyntaxError: Unexpected token 同じSyntaxErrorの上位テーマで、JSON以外の構文ミスも含めた全体像を確認できる
JavaScript TypeError: Failed to fetch 通信そのものが失敗するエラーで、本エラーとの切り分けに役立つ
JavaScript TypeError: Cannot read properties of undefined パース後のデータを参照する段階で頻発し、fetch処理の次に踏みやすい
Python requests.exceptions.ConnectionError API連携時のトラブルという点で共通し、サーバー側の応答確認の考え方が近い
404エラーとは?403との違いとユーザーを逃がさないカスタムページの作り方 HTMLエラーページが返る根本原因の一つで、なぜHTMLが届くのかを理解できる

【出典】参考URL

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/JSON_bad_parse :JSON.parseの構文エラーの意味とFirefoxの文言の根拠
https://developer.mozilla.org/en-US/docs/Web/API/Response/json :Response.json()がJSONをパースする仕様の根拠
https://developer.mozilla.org/en-US/docs/Web/API/Response/ok :res.okがHTTPステータス200番台を示す仕様の根拠
https://daveceddia.com/unexpected-token-in-json-at-position-0/ :旧V8の文言とHTMLレスポンスが原因になる挙動の確認

コメント

デプロイ太郎のSNSを見てみる!!