- ボタンをクリックしても反応せず、Consoleに「Cannot read properties of null (reading ‘addEventListener’)」と表示されることがあります。このエラーは、イベントを登録する対象の要素が見つからないまま、
addEventListener()を呼び出したときによく発生します。本記事では、原因を推測で直すのではなく、Console、セレクター、読み込み順、動的に追加される要素の順番で切り分ける方法を解説します。addEventListenerのnullエラーは何を意味する?
次のコードでは、HTMLからid="menu-button"の要素を探し、クリックイベントを登録しています。const button = document.querySelector("#menu-button"); button.addEventListener("click", () => { console.log("クリックされました"); });document.querySelector()は、一致する要素があれば最初の要素を返します。しかし、一致する要素がなければnullを返します。その状態でnull.addEventListener()を実行しようとするため、TypeErrorになります。 つまり、最初に調べるべきなのはaddEventListener()の書き方ではなく、直前の要素取得が成功しているかどうかです。最初にConsoleでエラーの場所を確認する
Chromeでは、ページ上で右クリックして「検証」を開き、Consoleタブを確認します。エラーの右側に表示されるファイル名と行番号をクリックすると、問題が起きたコードへ移動できます。 原因を確認するため、取得した値を一度出力します。const button = document.querySelector("#menu-button"); console.log(button);- 対象のHTML要素が表示される:要素取得には成功しています。
nullと表示される:セレクターか読み込み順に問題があります。- この行より前に別のエラーがある:先に発生したエラーで処理が止まっています。
原因1:CSSセレクターがHTMLと一致していない
querySelector()にはCSSと同じ形式のセレクターを渡します。idは#、classは.を付けます。<button id="menu-button" class="menu-button"> メニューを開く </button>
次のような小さな違いでも、要素は取得できません。// idで探す const buttonById = document.querySelector("#menu-button"); // classで探す const buttonByClass = document.querySelector(".menu-button");#menu-buttonと#menu_buttonの違いmenu-buttonに#や.を付け忘れている- HTMLでは
menuButton、JavaScriptではmenubuttonになっている - 別ページには対象のボタン自体が存在しない
idとclassを確認すると早く切り分けられます。原因2:HTMLが作られる前にJavaScriptを実行している
セレクターが正しくても、<head>内のスクリプトが先に実行されると、下に書かれたボタンはまだDOMに存在しません。
外部JavaScriptには<head> <script src="main.js"></script> </head> <body> <button id="menu-button">メニューを開く</button> </body>deferを付けると、HTMLの解析後にスクリプトを実行できます。<head> <script src="main.js" defer></script> </head>deferを付けられない構成では、DOMContentLoadedを利用できます。このイベントは、HTML文書の読み込みと解析が完了した時点で発生します。
スクリプトタグをdocument.addEventListener("DOMContentLoaded", () => { const button = document.querySelector("#menu-button"); button.addEventListener("click", () => { console.log("クリックされました"); }); });</body>の直前に置く方法もあります。ただし、複数のファイルがあるサイトでは、読み込み意図が分かりやすいdeferを使うと管理しやすくなります。 ローカル環境でも、対象のボタンより前にJavaScriptを実行して同じ状態を再現しました。ボタンがまだ作られていない時点では、querySelector()の結果がnullになり、addEventListener()を呼び出した箇所でエラーが発生します。 ボタンを先に記述してからJavaScriptを実行するように変更すると、要素を取得でき、クリックイベントが正常に動作しました。
修正前:対象要素が作られる前にJavaScriptを実行し、Consoleにnullエラーが表示された状態
修正後:HTMLのボタンを先に読み込み、クリックイベントが正常に実行された状態 原因3:対象要素がないページでも同じJavaScriptを読み込んでいる
WordPressなどでは、同じJavaScriptファイルを全ページで読み込むことがあります。トップページにはメニューボタンがあっても、別のテンプレートには存在しない場合、要素取得はnullになります。 要素があるページでだけ処理したい場合は、存在確認を入れます。
オプショナルチェーンを使うと短く書けます。const button = document.querySelector("#menu-button"); if (button) { button.addEventListener("click", () => { console.log("クリックされました"); }); }
ただし、必ず存在するはずの要素にこの書き方を使うと、HTMLの記述ミスを見逃しやすくなります。ページによって存在しないことが正常な要素に使い、必須要素の場合はエラーとして確認するなど、目的に応じて使い分けます。document .querySelector("#menu-button") ?.addEventListener("click", () => { console.log("クリックされました"); });原因4:イベント名や関数の渡し方が間違っている
addEventListener()へ渡すイベント名にはonを付けません。クリックなら"click"です。
登録時には関数を実行するのではなく、関数そのものを渡します。// 間違い button.addEventListener("onclick", handleClick); // 正しい button.addEventListener("click", handleClick);
引数を渡したい場合は、アロー関数の中から呼び出します。function handleClick() { console.log("クリックされました"); } // 間違い:この場で関数を実行している button.addEventListener("click", handleClick()); // 正しい:関数を渡している button.addEventListener("click", handleClick);button.addEventListener("click", () => { handleMenu("open"); });原因5:あとから追加した要素へイベントを登録している
APIの取得結果やボタン操作によってHTMLを追加する場合、追加前にquerySelector()を実行しても要素は見つかりません。
要素を追加したあとにイベントを登録するか、親要素でイベントを受け取る「イベント委譲」を使います。const item = document.querySelector(".new-item"); console.log(item); // 追加前なのでnull list.insertAdjacentHTML( "beforeend", '<button class="new-item">追加されたボタン</button>' );
親のconst list = document.querySelector(".item-list"); list.addEventListener("click", (event) => { const button = event.target.closest(".new-item"); if (!button || !list.contains(button)) { return; } console.log("追加されたボタンがクリックされました"); });.item-listは最初から存在するため、あとから追加された.new-itemのクリックも処理できます。複数のボタンへ同じイベントを個別登録しなくてよい点もメリットです。イベントは動いているのに画面が変わらない場合
フォーム送信でページが再読み込みされている
form内のボタンは、種類を指定しないと送信ボタンとして動作することがあります。単なる画面操作用ならtype="button"を指定します。
フォーム送信処理をJavaScriptで制御する場合は、<button type="button" id="menu-button"> メニューを開く </button>submitイベントで既定動作を止めます。const form = document.querySelector("#contact-form"); form.addEventListener("submit", (event) => { event.preventDefault(); console.log("入力内容を確認します"); });クリックされた場所が想定と違う
アイコンやspanを含むボタンでは、event.targetが内側の要素になることがあります。リスナーを登録した要素を取得したい場合はevent.currentTargetを使います。button.addEventListener("click", (event) => { event.currentTarget.classList.toggle("is-open"); });そのまま試せる完成例
次の例は、ボタンを押すと案内文の表示状態を切り替えます。外部ファイルで使う場合は、JavaScript側をmain.jsへ保存し、スクリプトにdeferを付けます。HTML
<button type="button" class="help-button" aria-expanded="false" aria-controls="help-message" > 解決方法を表示 </button> <p id="help-message" hidden> Consoleとセレクターから順番に確認しましょう。 </p> <script src="main.js" defer></script>JavaScript
この例では、要素の存在確認をしてからイベントを登録しています。また、const button = document.querySelector(".help-button"); const message = document.querySelector("#help-message"); if (button && message) { button.addEventListener("click", () => { const isOpen = button.getAttribute("aria-expanded") === "true"; button.setAttribute("aria-expanded", String(!isOpen)); message.hidden = isOpen; button.textContent = isOpen ? "解決方法を表示" : "解決方法を閉じる"; }); }aria-expandedの値と表示状態を揃えることで、ボタンの状態が支援技術にも伝わるようにしています。動かないときの確認チェックリスト
- DevToolsのConsoleで最初のエラーと行番号を確認する
console.log()で取得した要素がnullではないか確認する- HTMLの
id・classとセレクターを見比べる - 外部スクリプトに
deferを付けるか、DOM生成後に実行する - 対象要素が存在しないページでは、存在確認を入れる
- イベント名に
onを付けていないか確認する - コールバックを
handleClick()ではなくhandleClickとして渡す - 動的要素なら、追加後の登録またはイベント委譲を使う
- フォーム送信やリンク移動で結果が消えていないか確認する
- JavaScriptファイルのURLが404になっていないかNetworkタブで確認する
まとめ
addEventListener()が動かないときは、最初に対象要素を取得できているか確認します。querySelector()がnullなら、セレクター、JavaScriptの読み込み順、ページ内に要素が存在するかを順番に調べます。要素を取得できている場合は、イベント名、関数の渡し方、フォームの既定動作、動的要素の追加タイミングを確認しましょう。確認順序を決めておくと、コードをむやみに書き換えずに原因を特定できます。参考資料
JavaScriptのaddEventListenerが動かない原因|nullエラーを順番に解決
掲載内容は最終確認日時点の情報です。バージョンや環境によって表示・手順が異なる場合があります。重要な変更の前にはバックアップを取得してください。