HTML/CSSで骨組みを作ったあと、JavaScriptを使って「ボタンをクリックしたらメニューを開く」「入力欄の値を取得する」といった動的な処理を付けようとした際、以下のエラーで手が止まってしまったことはないでしょうか。
Uncaught TypeError: Cannot read properties of null (reading 'addEventListener')
Uncaught TypeError: null is not an object (evaluating 'button.addEventListener')
このエラーの原因は、addEventListener()の書き方ミスではなく、その直前のdocument.querySelector()が要素を見つけられず、null(該当なし)を返していることにあります。
HTMLに要素を書いたはずなのに、なぜJavaScriptから取得できないのか。
この記事では、querySelector()がnullになる典型的な原因と解決策、そしてエラーが出たときの確実なデバッグ手順を解説します。
querySelectorでnullになるときにまず確認すること
コードを複雑にいじる前に、まずは以下のチェックリストを上から確認してみてください。
- クラス名やID名の指定ミス(
.や#の付け忘れ)はないか - HTMLが読み込まれるより前にJavaScriptが実行されていないか(
<script>にdeferがあるか) - 現在のページに対象要素が本当に存在しているか(共通JSによる他ページでのエラーではないか)
- セレクタに一致する要素が本当にあるか、DevToolsのConsoleで確認したか
- クリックや通信のあとに動的追加される要素を、初期化時に取得しようとしていないか
querySelectorとは?
document.querySelector()は、CSSセレクタと同じ書き方を使って、HTML文書の中から条件に一致する最初の要素を1つ取得するメソッドです。
<button class="menu-btn">メニュー</button>
// クラス名で取得する
const button = document.querySelector('.menu-btn');
console.log(button); // <button class="menu-btn">メニュー</button>
CSSでスタイルを当てるときと同じルールがそのまま使えます。
document.querySelector('#header'); // ID名で取得(#)
document.querySelector('.js-tab'); // クラス名で取得(.)
document.querySelector('button'); // タグ名で取得
document.querySelector('[data-modal]'); // 属性セレクタで取得
条件に一致する要素がページ内に見つからなかった場合、querySelector()はエラーを出すのではなく、「該当なし」を意味するnullを返します。
なお、指定した文字列そのものがCSSセレクタとして不正な場合は、nullではなくSyntaxErrorが発生します。
原因1:クラス名・ID名の指定を間違えている
よくある原因の1つが、セレクタ文字列の記述ミスです。
<button class="js-button">送信</button>
// ✕ 取得できない例(ドットが抜けている)
const button = document.querySelector('js-button');
// → <js-button> という名前のHTMLタグを探しに行ってしまい、null になる
// ◯ 正しい例(クラス名には . を付ける)
const button = document.querySelector('.js-button');
よくある指定ミスには以下のようなものがあります。
- クラス名なのに
.を忘れている(例:'btn'ではなく'.btn') - ID名なのに
#を忘れている(例:'menu'ではなく'#menu') - HTML側のスペルとJS側のスペルが一致していない(大文字・小文字の違い、ハイフンとアンダースコアの違いなど)
原因2:JavaScriptを実行した時点でHTMLがまだ存在しない
HTML側には間違いなく<button class="js-button">が書かれているのにnullになる場合、よくある原因が「スクリプトの実行タイミング」です。
ブラウザはHTMLファイルを上から順番に読み込んで解析(パース)します。
<head>タグの中にそのまま<script>を書くと、HTMLの本文(<body>)が作られる前にスクリプトが実行されてしまいます。
<!-- ✕ 失敗する配置例 -->
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<script src="main.js"></script> <!-- この時点で下のbuttonはまだ存在しない -->
</head>
<body>
<button class="js-button">クリック</button>
</body>
</html>
// main.js
const button = document.querySelector('.js-button');
console.log(button); // null(ボタンが作られる前に探したため)
button.addEventListener('click', () => {
// TypeError: Cannot read properties of null
});
解決策1:scriptタグにdefer属性を付ける(推奨)
最もシンプルで現代的な解決策は、<head>内の<script>タグにdefer属性を付与することです。
<head>
<script src="main.js" defer></script>
</head>
deferを付けると、ブラウザはHTMLの解析を止めずに外部スクリプトを読み込み、HTMLの解析が完了してからスクリプトを実行します。
※deferはここで例示しているsrc付きの外部スクリプトで使用します。インラインの通常スクリプト(<script>...</script>)に付けても効果はありません。
解決策2:bodyの閉じタグ直前に書く
HTMLの末尾、</body>の直前に<script>を配置する方法です。
<body>
<button class="js-button">クリック</button>
<!-- HTML要素がすべて読み込まれた後に実行される -->
<script src="main.js"></script>
</body>
解決策3:DOMContentLoadedイベントを待つ
インラインスクリプトなどで読み込み位置を変えられない場合は、DOMContentLoadedイベントを使ってDOMの準備完了を待つ方法もあります。
document.addEventListener('DOMContentLoaded', () => {
const button = document.querySelector('.js-button');
// HTMLに対象要素が存在すれば、この時点で取得できる
});
基本的には、<script src="..." defer></script>を使う構成にしておくのが最も扱いやすくおすすめです。
原因3:そもそも対象要素がそのページに存在しない
WordPressサイトや複数ページのWebサイトで、1つの共通JavaScriptファイル(common.jsなど)を全ページで読み込んでいる場合によく起こります。
例えば、「お問い合わせページにしかない送信ボタン」に対する処理を共通ファイルにそのまま書いているケースです。
// お問い合わせページ以外(トップページなど)ではこの要素が存在しない
const submitButton = document.querySelector('.js-submit-btn');
// トップページを開くと、submitButton が null のためここでエラーになり、
// このスクリプト内の後続処理が実行されなくなる
submitButton.addEventListener('click', () => {
// ...
});
対策:ページによって存在しないのが正常ならif文で確認する
対象要素が特定のページにしか存在しない設計であれば、イベントを設定する前にif文で存在確認(nullチェック)を行います。
const submitButton = document.querySelector('.js-submit-btn');
// 対象要素が存在するページでのみ処理を実行する
if (submitButton) {
submitButton.addEventListener('click', () => {
console.log('送信処理を実行');
});
}
このガード処理を入れておけば、その要素がないページで実行されてもエラーにならず、共通JavaScript内の後続処理が途中で止まるのを防げます。
※本来すべてのページに存在するはずの要素(グローバルナビゲーションなど)がnullになっている場合は、ifで隠すのではなくセレクタの記述や読み込みタイミング(原因1や原因2)を修正してください。
Cannot read properties of nullは何を意味している?
ブラウザのConsoleに表示されるエラーメッセージの意味を分解してみましょう。
Uncaught TypeError: Cannot read properties of null (reading 'addEventListener')
これは、「nullに対して.addEventListenerというプロパティやメソッドを読み取ろうとしましたが、nullには中身がないので実行できません」という意味です。
const button = document.querySelector('.btn');
// ↑ 要素が見つからず button は null
button.addEventListener('click', () => {
console.log('クリック');
});
// ↑ null に対して addEventListener() を呼ぼうとしてエラーになる
このエラーが出たときは、「addEventListenerの書き方が間違っている」のではなく、「その手前にあるbutton変数の中に要素がちゃんと入っているか」を疑うのが正しいアプローチです。
const button = document.querySelector('.btn');
console.log(button); // まず中身を出力して確認する
querySelectorとquerySelectorAllの違い
ページ内に同じクラス名の要素が複数ある場合、querySelector()とquerySelectorAll()の使い分けが必要です。
<ul class="list">
<li class="item">項目1</li>
<li class="item">項目2</li>
<li class="item">項目3</li>
</ul>
querySelector()は「最初の1件」だけを返す
const item = document.querySelector('.item');
console.log(item); // <li class="item">項目1</li> だけが取れる
querySelectorAll()は「全件」をNodeList(配列のような形式)で返す
const items = document.querySelectorAll('.item');
console.log(items); // NodeList(3) [li.item, li.item, li.item]
querySelectorAll()で取得したNodeListには、直接.addEventListener()を付けることはできません。
要素を1つずつ取り出して設定します。
const items = document.querySelectorAll('.item');
// ✕ エラーになる(NodeList には addEventListener メソッドがない)
// items.addEventListener('click', () => {});
// ◯ 1件ずつ取り出してイベントを設定する
items.forEach(item => {
item.addEventListener('click', () => {
console.log('クリックされました:', item.textContent);
});
});
なお、querySelectorAll()が返すNodeListは静的(static)です。
取得した後に同じクラスの要素がDOMに追加されても、すでに取得済みのitemsへ自動的に反映されるわけではありません。
後から追加した要素へclickイベントを設定する場合の対処方法は、イベントデリゲーションの記事で詳しく解説しています。
querySelectorAllは0件でもnullにならない
知っておくと役立つ違いとして、該当する要素が1つもなかったときの戻り値があります。
// 1件も見つからない場合
const single = document.querySelector('.not-found');
console.log(single); // null
const multiple = document.querySelectorAll('.not-found');
console.log(multiple); // NodeList [] (中身が空のNodeList)
querySelectorAll()は該当なしでもnullではなく「空のリスト(要素数0)」を返すため、items.forEach(...)を実行してもエラーで止まることはありません(単にループが0回で終了します)。
後から追加された要素は最初のquerySelectorでは取得できない
JavaScriptで動的にHTML要素を追加(生成)するUIを作る際にも、取得のタイミングによるトラブルが発生します。
// 1. ページ読み込み時に要素を探す(この時点ではまだ画面にない)
const dynamicButton = document.querySelector('.dynamic-button'); // null
// 2. ボタン操作などで後から要素をHTMLに追加する
container.insertAdjacentHTML(
'beforeend',
'<button class="dynamic-button">追加ボタン</button>'
);
// 3. 後からHTMLに追加されても、変数 dynamicButton が自動で更新されるわけではない
querySelector()は、「その行を実行したまさにその瞬間のDOM」から要素を探して返します。
あとからHTMLに要素が追加されても、過去に実行した変数のnullが自動的に実要素に置き換わることはありません。
動的に作られる要素を操作したい場合は、要素をHTMLに追加した直後に取得し直すか、親要素にイベントを設定して受け取る手法(イベントデリゲーション)を用います。
querySelectorで取得できたか確認する方法
要素がうまく取れないときは、コードを書き直す前にブラウザの「Console(コンソール)」を使って状態を切り分けます。
1. console.logで中身を見る
const target = document.querySelector('.target');
console.log(target);
Consoleに実際のHTML要素が出力されれば取得成功です。
nullと表示されたら、セレクタ・実行タイミング・そのページに対象要素が存在するかを確認します。
2. DevToolsのConsoleで直接実行してみる
ブラウザの開発者ツール(F12キーや右クリック「検証」)を開き、Consoleタブで直接以下を打ち込んでEnterを押してみてください。
document.querySelector('.target')
- ここで要素が取れる場合:セレクタ自体は合っています。初回のJavaScript実行時にはまだ要素が存在していなかった可能性があります。
deferなどの読み込みタイミングや、要素が後から追加されていないかを確認します。 - ここでも
nullになる場合:クラス名のタイポ(スペルミス)、.や#の付け忘れ、またはそのページに対象要素が存在していません。
querySelectorでnullになるときの確認手順
トラブルが起きたときは、以下のステップ順に確認していけば原因を絞り込めます。
- DevToolsのConsoleで直接実行する:ブラウザのConsoleに
document.querySelector('セレクタ')を貼り付けてみる - セレクタの記述を確認する:クラスの
.、IDの#、綴りの間違いがないか確認する - HTMLの読み込みタイミングを確認する:
<script src="..." defer></script>になっているか確認する - ページの要素存在確認:そのページに対象要素が本来存在するか確認する。ページによって存在しないことが正常な要素なら、
if (target) { ... }で存在確認してから処理する - 複数要素・動的要素の確認:複数なら
querySelectorAll、あとから増える要素なら追加処理の順序を見直す
querySelector・querySelectorAll・getElementByIdの使い分け
要素を取得する主要なメソッドの特徴のまとめです。
| メソッド | 取得対象 | 見つからなかった場合の戻り値 | 主な使いどころ |
|---|---|---|---|
document.querySelector() |
指定セレクタに一致する最初の1要素 | null |
単一のボタン、フォーム欄、モーダルなど、1つだけ要素を取得したいとき |
document.querySelectorAll() |
指定セレクタに一致するすべての要素 | 空のNodeList [](静的) |
タブ、アコーディオン、リスト項目など、複数の要素をループ処理したいとき |
document.getElementById() |
指定したIDを持つ1要素 | null |
セレクタ解釈が不要で、ID名をピンポイントで指定したいとき |
まとめ
document.querySelector()で要素が取得できずnullになるのは、JavaScriptでDOM操作を始めたときによく遭遇するトラブルです。
- エラーの本質:
Cannot read properties of nullは、イベント設定のミスではなく「手前で要素の取得に失敗している」合図 - 書き方の確認:クラス名は
.、ID名は#を忘れていないかチェックする - タイミングの調整:HTMLが読み込まれる前にJSが動かないよう、外部スクリプトには
deferを付ける - 実務での安全策:ページによって存在しない要素を扱う場合は、要素取得後に
if (element) { ... }で存在チェックを行う
「エラーが出たら、まずconsole.log()で要素が取れているか確かめる」。
この切り分けの基本を押さえておけば、今後のあらゆるDOM操作で迷わなくなります。