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になるときにまず確認すること

コードを複雑にいじる前に、まずは以下のチェックリストを上から確認してみてください。

  1. クラス名やID名の指定ミス(.や#の付け忘れ)はないか
  2. HTMLが読み込まれるより前にJavaScriptが実行されていないか(<script>にdeferがあるか)
  3. 現在のページに対象要素が本当に存在しているか(共通JSによる他ページでのエラーではないか)
  4. セレクタに一致する要素が本当にあるか、DevToolsのConsoleで確認したか
  5. クリックや通信のあとに動的追加される要素を、初期化時に取得しようとしていないか

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になるときの確認手順

トラブルが起きたときは、以下のステップ順に確認していけば原因を絞り込めます。

  1. DevToolsのConsoleで直接実行する:ブラウザのConsoleにdocument.querySelector('セレクタ')を貼り付けてみる
  2. セレクタの記述を確認する:クラスの.、IDの#、綴りの間違いがないか確認する
  3. HTMLの読み込みタイミングを確認する:<script src="..." defer></script>になっているか確認する
  4. ページの要素存在確認:そのページに対象要素が本来存在するか確認する。ページによって存在しないことが正常な要素なら、if (target) { ... }で存在確認してから処理する
  5. 複数要素・動的要素の確認:複数なら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操作で迷わなくなります。