//
2026.09.25/5 min read#language#modern-web

JavaScriptを「ライブラリなしで」書ける範囲を広げる——fetch・DOM・イベント

小さなサイトの「動き」は、ブラウザ標準のAPIだけでかなり書けます。私が素のJavaScriptで書くときに押さえている、fetchのエラー処理、DOMの組み立て方、イベントの付け方と外し方の3点をまとめました。

お知らせの一覧を読み込む、タブを切り替える、フォームを非同期で送る。コーポレートサイトやLPで求められる「動き」の多くは、ライブラリを入れなくてもブラウザ標準のAPIで書けます。私が押さえているのは fetchは「失敗」を自分で判定する、DOMは文字として流し込まない、イベントは親に1つだけ付けて外し方も決めておく の3点です。ここを覚えておくと、依存パッケージを増やさずに済む場面が広がります。

fetch:404では失敗しない、を前提に書く

fetch() でいちばん勘違いされやすいのは、エラーの扱いです。MDNには、サーバーが404や504のようなエラーのステータスを返しても fetch() のPromiseはrejectしない、と明記されています。rejectするのは、URLの形式が不正なときやネットワークエラーのときだけです(MDN: fetch())。

つまり、try/catch で囲んだだけでは、404のページを「成功」として処理してしまいます。私は次の形を1つ用意して使い回しています。

async function getJSON(url) {
  const res = await fetch(url, { signal: AbortSignal.timeout(8000) });
  if (!res.ok) {
    throw new Error(`HTTP ${res.status}`);
  }
  return res.json();
}

res.ok でステータスを確かめ、AbortSignal.timeout() で待ち時間の上限を決めています。時間切れのときは TimeoutError という名前のエラーで中断されます(MDN: AbortSignal.timeout())。こちらはMDNで2024年4月から主要ブラウザで使えるとされている比較的新しい機能なので、古い端末の利用者が多い案件では対応範囲を確認してから使います。

ディレクションの目線でいうと、ここで決めておきたいのは「失敗したとき画面に何を出すか」です。読み込めなかったときの文言をカンプの段階で1行決めておくだけで、実装時の迷いが減ります。

DOM:innerHTMLに外のデータを流し込まない

取ってきたデータを画面に出すとき、文字列を組み立てて innerHTML に入れる書き方は手軽です。ただ、CMSやAPIから来た文字列をそのまま入れると、中にタグが混ざっていた場合にそれがHTMLとして解釈されてしまいます。

私は、外から来た値は textContent で入れる、と決めています。

function renderNews(list, items) {
  const frag = document.createDocumentFragment();
  for (const item of items) {
    const li = document.createElement("li");
    const a = document.createElement("a");
    a.href = item.url;
    a.textContent = item.title;
    li.append(a);
    frag.append(li);
  }
  list.replaceChildren(frag);
}

行数は増えますが、「どこが文字でどこが構造か」がコードから読めるようになります。繰り返しの多い部品なら、HTML側に template 要素で雛形を置いておき、複製して値だけ差し込む形にすると、マークアップの確認をデザイナーとHTMLのまま行えます。

イベント:親に1つ付けて、closest()で拾う

ボタンが10個あるから10個にイベントを付ける、という書き方は、あとから要素を追加したときに付け忘れが起きます。私は一覧やタブのような部品では、親要素に1つだけイベントを付けています。

tabs.addEventListener("click", (e) => {
  const btn = e.target.closest("button[data-tab]");
  if (!btn) return;
  showTab(btn.dataset.tab);
});

closest() は、自分自身か祖先のうちセレクタに一致する最も近い要素を返し、なければ null を返します(MDN: Element.closest())。ボタンの中のアイコンがクリックされても、ボタン本体を拾えるのが便利なところです。

もう1つ覚えておくと楽になるのが、イベントの外し方です。addEventListener のオプションに signal を渡しておくと、対応する AbortController の abort() を呼んだ時点でリスナーが外れます(MDN: addEventListener())。モーダルを開いている間だけEscキーを監視する、といった処理を、関数の参照を持ち回らずに書けます。1回だけ動けばいい処理なら once: true で足ります。

どこからライブラリを入れるか

素で書ける範囲が広がっても、何でも自作すればいいわけではありません。私の線引きは、画面の状態が3つ以上の部品で共有されはじめたらフレームワークを検討する、というものです。お知らせ一覧やタブ、アコーディオンのように部品の中で完結する動きなら、標準APIのほうが読みやすく、引き継いだ人も追いやすい。逆に、カートの中身がヘッダー・一覧・詳細で同時に変わるような画面は、状態を管理する仕組みがあったほうが結果的に短くなります。

明日から試せる一手

手元のコードで fetch() を使っている箇所を探し、res.ok を確かめているかを見てください。確かめていなければ、上の getJSON のような関数を1つ作って置き換え、読み込めなかったときに画面へ出す文言も1行決めておく。エラー時の表示が決まるだけで、その部品は「動く」から「運用できる」に一歩近づきます。