2026.09.23 泊まれる場所4,223 立ち寄りスポット59,693 営業中を確認したキャンプ場1,005 note を読む

Cannot read properties of nullの原因|scriptがHTMLより先に動くと、ボタンはまだ無い

ボタンに処理を付けたのに、押しても動かない。
コンソールには Cannot read properties of null の文字。
原因の多くは、scriptがボタンより先に動いていることでした。

ボタンにクリックの処理を付けたはずなのに、押しても何も起きない。
コンソールを開くと「Uncaught TypeError: Cannot read properties of null (reading 'addEventListener')」と出ている。
このエラーのいちばん多い原因は、scriptがHTMLより先に動いて、まだ存在しない要素を探していることだ。
直し方は、scriptタグに defer を付けるのが一番手軽だと僕は思う。

僕の社内のコーディング規約では、scriptタグには必ず defer を付けると決めている。
このエラーを、書き方の段階で起こさないための決まりだ。
今回は、なぜそれで防げるのかを順に書いておく。

エラーの文の読み方

「Cannot read properties of null」は、nullの中身を読もうとした、という意味だ。
後ろのカッコの「reading 'addEventListener'」は、読もうとした名前を表している。
つまり「addEventListenerを呼ぼうとした相手が、nullだった」と書いてある。

今日、Node.js(ChromeとエンジンがおなじV8)で確かめると、次の1行で同じ文が出た。

const el = null;
el.addEventListener('click', () => {});
// TypeError: Cannot read properties of null (reading 'addEventListener')

MDNのエラーの解説でも、nullやundefinedに対してメソッドを呼ぶと、このTypeErrorになると説明されている。
Firefoxでは「el is null」のように、書き方の違う文になる。
文が違っても、起きていることは同じだ。

なぜ要素がnullになるのか

よくある書き方は、こんな形だと思う。

const btn = document.getElementById('send_btn');
btn.addEventListener('click', send);

MDNによると、getElementById は、指定したidの要素が文書に無ければnullを返す。
エラーを出すのではなく、黙ってnullを返すところがやっかいだ。
そのnullに対して次の行でaddEventListenerを呼んだ瞬間に、さっきのエラーになる。

要素が「無い」理由は、だいたい次の3つに分かれる。

  • scriptが先に動いた:headの中や、ボタンより上にscriptを書いていて、読み込んだ時点でボタンがまだ作られていない
  • idの綴りが違う:MDNにあるとおり、idは大文字と小文字を区別する。「Send_btn」と「send_btn」は別物になる
  • そのページに要素が無い:共通のjsを全ページで読んでいて、ボタンの無いページでも同じ処理が走っている

いちばん多いのは、scriptが先に動くこと

ブラウザはHTMLを上から順に読む。
headの中に普通のscriptタグがあると、そこで読み込みを止めて、scriptを先に実行する。
その時点では、bodyの中のボタンはまだ作られていない。
だから getElementById は何も見つけられず、nullを返す。

同じコードでも、bodyの一番下に書いていた時は動いていたのに、headへ移したら壊れた、ということが起きる。
コードの中身は正しいので、原因に気づきにくいと思う。

直し方1:scriptタグに defer を付ける

MDNのscript要素の解説では、deferは「文書の構文解析が終わった後、DOMContentLoadedイベントの前に実行する」と書かれている。
HTMLを最後まで読んでから動くので、ボタンはもう存在している。
deferの付いたscriptは、書いた順に実行されることも保証されている。

注意が2つある。
deferが効くのは、src属性で外部ファイルを読む時だけで、タグの中に直接書いたscriptには効かない。
type="module"のscriptは、何も付けなくても初めから延期される。

僕の規約でCSSとJSを外部ファイルにする、と決めているのは、このdeferを確実に効かせるためでもある。

直し方2:DOMContentLoaded を待つ

scriptタグをいじれない時は、コードの側で待つ方法がある。

document.addEventListener('DOMContentLoaded', () => {
const btn = document.getElementById('send_btn');
btn.addEventListener('click', send);
});

MDNによると、DOMContentLoadedは、HTMLを読み終えてDOMが組み上がった時点で発生する。
画像やスタイルシートの読み込みは待たない。
deferのscriptが実行し終わった直後に来るので、ほぼ同じ時点で動くと考えてよいと思う。

直し方3:ボタンの無いページを先に弾く

共通のjsを全ページで読んでいる場合は、deferを付けても直らない。
そもそもそのページに、ボタンが無いからだ。
この時は、見つからなかったら何もしないように書く。

const btn = document.getElementById('send_btn');
if (btn) {
btn.addEventListener('click', send);
}

オプショナルチェーン btn?.addEventListener(...) でも同じことができる。
ただ、これは「無くてもよい」ことがはっきりしている時だけに使っている。
本当はあるはずの要素が無い時までエラーを消してしまうと、idの綴り違いに気づけなくなるからだ。

どれから疑うか

このエラーが出たら、僕は次の順で見ている。

  • scriptタグの位置と、defer が付いているか
  • HTMLのidと、JSに書いたidが一字一句同じか(大文字・小文字・ハイフンとアンダースコア)
  • エラーが出ているページに、その要素が本当にあるか

idの照合は、開発者ツールのコンソールで document.getElementById('send_btn') と打てばすぐ分かる。
nullが返ってきたら、その名前の要素はそのページに無い。

JavaScriptの別のつまずきは、fetchが404でもcatchに入らない話や、イベントバブリングの話にも書いた。
どれも、コードは正しいのに動く順番や前提が違っていた、という種類の失敗だと思う。

出典

—

ほかにも書いています

受託・note・スタンプ

※受託の相談とnoteは、運営者が個人で受けている・書いているものです。

技術ブログ一覧へ戻る