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・スタンプこういう仕組みを作る相談を受けています
毎日の更新・データ集め・集計の自動化/3万円〜収益がいくらだったか、どの施策が外れたか
note / 個人の視点で掘り下げていますスタンプラリーに挑戦する
行った場所は、現地チェックインで記録に残せます※受託の相談とnoteは、運営者が個人で受けている・書いているものです。
