かの有名なマーフィーの法則にこんな言葉があります─「失敗する余地があるなら、失敗する」。これは、プログラミングの世界にも当てはまります。アプリケーション作りに、バグやその他の問題の発生はつきもの。JavaScriptでエラーが発生するのは、何も不思議なことではありません。

このような問題を、ユーザーに迷惑をかける前に、いかに解決できるか。これこそが、ソフトウェア製品の成功を左右します。そして、JavaScriptと言えば、すべてのプログラミング言語の中でも、その平均的なエラー処理設計で知られています。

JavaScriptでアプリケーションを開発していると、高い確率で一度はデータ型について失敗するはず。その他にも、undefinedをnullにしてしまったり、三重等号演算子(===)を二重等号演算子(==)にしてしまったり…というミスが発生するかもしれません。

間違いを犯すのは人間のさが。これを踏まえた上で、JavaScriptのエラーに対処する上で大事な点をご紹介したいと思います。

この記事では、JavaScriptのよくあるエラーに触れた上で、それを特定して解決する方法を見ていきます。また、実際の運用環境で効率的にエラーを処理するためのヒントもいくつかあります。

それでは、始めましょう。

JavaScriptでよくあるエラーの解決方法について動画での解説もご用意しています。

JavaScriptのエラーとは

プログラミングにおけるエラーとは、プログラムが正常に機能していない状況を指します。例えば、存在しないファイルを開こうとしたり、ネットワークに接続されていない状態でウェブベースのAPIエンドポイントにアクセスしようとしたりするなど。プログラムが指示された仕事を処理できない状態です。

このような状況に直面すると、プログラムはユーザーに対しエラーを投げかけ、「どのように進めてよいかわからない」と伝えます。つまり、エラーに関する情報が可能な限り収集され、それが表示されます。

利用者が「404」のような技術的なエラーを必死で「解読」する必要がないように、こうした状況を予測し、前もって対処することが重要です。例えば、もっとわかりやすいメッセージとして「ページが見つかりませんでした」などが表示できます。

JavaScript におけるエラーは、プログラミングエラーが発生したときに表示されるオブジェクトです。このオブジェクトには、エラーの種類、エラーの原因となったステートメント、およびエラーが発生したときのスタックトレースに関する十分な情報が含まれます。また、JavaScriptでは、プログラミングによりエラータイプを個別に作成し、問題をデバッグする際に表示される情報量を増やすことができます。

エラーのプロパティ

JavaScriptのエラーの定義がわかったところで、中身を見ていきましょう。

JavaScriptのエラーには標準的な、そして、手を加えることで表示されるようになる情報があります。これを確認することで、エラーの原因と影響を理解していくことができます。デフォルトでは、JavaScriptのエラーは3種類の情報で構成されます。

  1. Message:エラーメッセージを格納する文字列
  2. Name:発生したエラーの種類 (次のセクションで深く掘り下げます)
  3. Stack:エラー発生時に実行されていたコードのスタックトレース

さらに、「columnNumber」、「lineNumber」、「fileName」といった詳しい情報を利用することも可能です。しかし、これらのプロパティは標準的なものではなく、JavaScriptアプリケーションから生成されるエラーオブジェクトに存在することも、そうでないこともあります。

スタックトレースについて

スタックトレースとは、例外や警告などのイベントが発生したときに、直前にプログラムが実行していたメソッドを一覧で示したものです。例外を伴うスタックトレースは例えば、以下のようになります。

スタックトレースの例
スタックトレースの例

見ての通り、まずエラー名とメッセージが、その後に呼び出されたメソッドの一覧が表示されます。各メソッド呼び出しには、そのソースコードの場所と呼び出されたコードが記載されます。このデータを用い、コードベース内を移動し、どのコードがエラーを引き起こしているかを特定することができます。

このメソッドは、処理の順序を反映するかたちで表示されます。これにより、例外が出現した場所と、それがどのように伝搬していったのかを確認できます。例外の「catch」を実装すれば、例外がスタックを伝搬しプログラムをクラッシュさせてしまうことはありません。しかし、致命的なエラーをそのままにし、意図的にプログラムをクラッシュさせるケースも考えられます。

エラーと例外

多くの人が、エラーと例外を混同しています。しかし、両者には若干の、そして大事な違いがありますのでご注意ください。

わかりやすく理解できるように、簡単な例を挙げてみましょう。以下のようにして、JavaScriptでエラーを定義できます。

const wrongTypeError = TypeError("Wrong type found, expected character")

そして、このようにしてwrongTypeErrorオブジェクトが例外となります。

throw wrongTypeError

しかし、多くの人が、省略形(エラーオブジェクトを投げながら、同時に定義する方法)を使っています。

throw TypeError("Wrong type found, expected character")

これは標準的なやり方です。しかし、多くの開発者が例外とエラーをごっちゃにしてしまう原因の一つでもあります。したがって、効率化のために省略形を使うにしても、基本を知っておくことが肝要です。

JavaScriptのエラーの種類

JavaScriptには、あらかじめ定義されたさまざまなエラータイプがあります。これは、アプリケーションで明示的にエラーの処理を行い限り、JavaScriptランタイムによって自動的に選択、定義されます。

続いては、JavaScriptで最も一般的なエラーの種類、そして、それぞれがいつ、どのような理由で発生するのかを見てみましょう。

RangeError

RangeErrorは、変数に有効な値の範囲外にある値が設定されたときに表示されます。これは通常、関数に引数として値を渡すときに発生し、与えられた値が関数のパラメータの範囲内にないことを示します。正しい値を渡すためには、引数に取り得る値の範囲を知る必要があるため、サードパーティのライブラリのドキュメントが不十分な場合、このエラーの解決には手間がかかる可能性があります。

RangeErrorが発生する一般的なパターンを、以下に複数示します。

  • Arrayコンストラクタで不正な長さの配列を作成しようとした
  • toExponential()toPrecision()toFixed()などのメソッドに不正な値を渡した
  • normalize()のようなメソッドに不正な値を渡した

ReferenceError

ReferenceErrorは、コード内の変数の参照に何かしらの問題があることを意味します。変数を使用する前に値を定義するのを忘れたか、またはコードでアクセスできない変数を使用しようとしている可能性があります。いずれにしても、スタックトレースを確認することで、問題のある変数の参照を見つけ、修正にあたることができます。

ReferenceErrorが発生する一般的な理由としては、以下のようなものがあります。

  • 変数名のタイプミスをした
  • ブロックスコープの変数にスコープ外からアクセスしようとした
  • 読み込み前に、外部ライブラリのグローバル変数(jQueryの$など)を参照しようとした

SyntaxError

コードの構文に誤りがあることを意味します。修正するのが最も簡単なものの1つです。JavaScriptは(コンパイラではなく)スクリプト言語ですので、スクリプトにあるエラーがあると、実行時に、このようなエラーが返されます。コンパイル言語では、このようなエラーはコンパイル時に発見されます。したがって、エラーが解決されるまで、アプリのバイナリは作成されません。

SyntaxErrorsが発生する一般的な理由には、次のようなものがあります。

  • 逆コンマの欠落
  • 括弧の閉じの欠落
  • 中括弧や他の文字の不適切な利用

リンターを使って、このようなエラーを早期発見できるようにするのがおすすめです。

TypeError

TypeErrorは、JavaScriptアプリケーションで最も一般的なエラーの一つです。このエラーは、ある値が特定の想定された型でないことが判明したときに表示されます。よくあるケースは以下の通りです。

  • メソッドでないオブジェクトを呼び出した
  • nullまたはundefinedオブジェクトのプロパティにアクセスしようとした
  • 文字列を数値として扱った(またはその逆)

TypeErrorが発生する可能性は他にもたくさんあります。これ(そして対策も)については、後で触れることにします。

InternalError

InternalErrorは、JavaScriptのランタイムエンジンで例外が発生したときに表示されます。これはコードの問題を意味することも、そうでないこともあります。

主に、InternalErrorは以下の2つの状況で発生します。

  • JavaScriptランタイムのパッチやアップデートに例外を返すバグがある(これは稀ではありますが)
  • JavaScriptエンジンにとって大きすぎるエンティティがコード内に存在する(例:switch文のcaseの分岐が多すぎる、配列初期化子が大きすぎる、再帰が多すぎる)

このエラーを解決する理想的な方法は、エラーメッセージから原因を特定し、可能であればアプリのロジックを再構築して、JavaScriptエンジンの作業負荷が急激に上昇してしまうのを避けることです。

URIError

URIErrorは、decodeURIComponentのようなグローバルなURI処理関数が間違った方法で使用されたときに表示されます。これは通常、メソッド呼び出しに渡されたパラメータがURI標準に準拠しておらず、メソッドによって適切にパースされなかったことを意味します。

通常、引数に問題がないかを調べるだけで対処できるので簡単です。

EvalError

EvalErrorは、eval()関数の呼び出しで問題が発生したことを意味します。eval()関数は、文字列に格納されたJavaScriptコードを実行するのに使用されます。しかし、eval()関数の使用はセキュリティ上の問題から推奨されておらず、現在のECMAScriptの仕様ではEvalErrorクラスは返されないため、このエラーはあくまでも古いJavaScriptコードとの後方互換性を維持するために存在しています。

古いバージョンのJavaScriptで作業していると、このエラーに遭遇する可能性があります。いずれにせよ、eval()関数の呼び出しで実行されたコードに例外がないかどうか調査するのが得策です。

独自のエラータイプの作成

JavaScriptには、デフォルトで、ほとんどの状況を網羅できるだけのエラータイプクラスがあります。しかし、必要であれば、その他にもエラータイプを作成することができます。この柔軟性を支えるのが、JavaScriptがthrowコマンドで文字通り何でも返すことができるという性質です。

技術的に、以下のようなステートメントは完全に有効です。

throw 8
throw "An error occurred"

しかし、プリミティブ型では、その型や名前、付随するスタックトレースなど、エラーの詳細についてはわかりません。これに対処し、プロセスを標準化するために、Errorクラスがあります。尚、例外を表示する際にプリミティブ型を使用することは推奨されていません。

Errorクラスを拡張して、独自のエラークラスを作成することができます。その基本的な例は以下の通りです。

class ValidationError extends Error {
    constructor(message) {
        super(message);
        this.name = "ValidationError";
    }
}

これを、次のように使うことができます。

throw ValidationError("Property not found: name")

そしてinstanceofキーワードと組み合わせます。

try {
    validateForm() // code that throws a ValidationError
} catch (e) {
    if (e instanceof ValidationError)
    // do something
    else
    // do something else
}

JavaScriptのよくあるエラー10選

エラーの種類と、独自のエラータイプの作成方法を理解したところで、次はJavaScriptのコードを書くときに直面しがちなエラーを見ていきましょう。

よくあるJavaScriptのエラーについて動画での解説もご用意しています。

1. Uncaught RangeError

このエラーは、Google Chromeを使用中に、いくつかの状況下で発生します。まず、再帰関数を呼び出したものの終了しないケースです。これは、Chrome Developer Consoleで確認することができます。

再帰関数の呼び出しに関連したRangeErrorの例
再帰関数の呼び出しに関連したRangeErrorの例

これを解決するには、再帰関数を打ち切るポイントを正しく定義するようにしましょう。このエラーが発生するもう一つのパターンとして、関数のパラメータの範囲外の値を渡してしまった可能性もあります。以下がその例です。

toExponential()呼び出しに関連したRangeErrorの例
toExponential()呼び出しに関連したRangeErrorの例

通常、エラーメッセージには、コードのどこに問題があるのかが表示されます。その箇所を修正すれば、問題は解決するはずです。

 toExponential()関数呼び出しの出力
toExponential()関数呼び出しの出力

2. Uncaught TypeError: Cannot set property

このエラーは、未定義の参照にプロパティを設定しようとしたことを意味します。例えば、このようなコードです。

var list
list.count = 0

出力は以下のようになります。

TypeErrorの例
TypeErrorの例

このエラーを解決するには、プロパティにアクセスする前に、参照を値で初期化します。すると以下のようになります。

TypeErrorの解決方法
TypeErrorの解決方法

3. Uncaught TypeError: Cannot read property

これは、JavaScriptで最もよく発生するエラーの一つです。このエラーは、未定義のオブジェクトに対してプロパティを読み込んだり、関数を呼び出したりしようとしたときに表示されます。Chrome Developerコンソールで以下のコードを実行すると、このエラーが表示されます。

var func
func.call()

出力は以下の通りです。

未定義の関数に関連したTypeErrorの例
未定義の関数に関連したTypeErrorの例

このエラーには数多くの原因が考えられますが、その中の一つが未定義のオブジェクトです。また、UIのレンダリング時にstateの初期化がうまくいっていない可能性もあります。Reactアプリケーションでの実際の例が以下の通りです。

import React, { useState, useEffect } from "react";

const CardsList = () => {

    const [state, setState] = useState();

    useEffect(() => {
        setTimeout(() => setState({ items: ["Card 1", "Card 2"] }), 2000);
    }, []);

    return (
        <>
            {state.items.map((item) => (
                <li key={item}>{item}</li>
            ))}
        </>
    );
};

export default CardsList;

アプリは空のstateコンテナから始まり、2秒間の遅延の後、複数のアイテムが提供されます。この遅延は、ネットワークの呼び出しを模倣するために設けられています。ネットワークが超高速であっても、わずかな遅延が発生するため、コンポーネントは少なくとも1回はレンダリングされます。このアプリを実行しようとすると、次のエラーが表示されます。

ブラウザでTypeErrorのスタックトレースを表示
ブラウザでTypeErrorのスタックトレースを表示

これは、レンダリング時にstateコンテナが未定義であり、そこにプロパティitemsが存在しないことが原因です。このエラーを解決するのは簡単です。stateコンテナに初期のデフォルト値を設定しましょう。

// ...
const [state, setState] = useState({items: []});
// ...

これで、設定した遅延時間が経過すると、アプリで以下のような出力が実行されるようになります。

コードの出力
コードの出力

コードごとに具体的な修正のしかたは異なるかもしれませんが、ここで大事なのは、変数を使用する前に適切に初期化することです。

4. TypeError: ‘undefined’ is not an object

このエラーは、Safariで未定義のオブジェクトのプロパティにアクセスしようとしたとき、メソッドを呼び出そうとしたときに表示されます。上記と同じコードを実行すれば、このエラーが確認できるはずです。

関数が未定義であることに関連したTypeErrorの例
関数が未定義であることに関連したTypeErrorの例

このエラーの解決方法も同じです。変数を正しく初期化し、プロパティやメソッドにアクセスしたときに変数が未定義になっていないようにしましょう。

5. TypeError: null is not an object

これもまた、先のエラーと同様です。唯一の違いは、プロパティまたはメソッドにアクセスしたいオブジェクトがundefinedではなくnullであることです。以下のコードを実行し、このエラーを再現することができます。

var func = null

func.call()

以下が、その出力です。

null関数によるTypeErrorの例
null関数によるTypeErrorの例

nullは明示的に変数に設定された値であり、自動で代入されるものではありません。このエラーは、自分でnullを設定した変数にアクセスしようとした場合のみ表示されます。そのため、自分の書いたロジックが正しいかどうか、もう一度見直してみる必要があります。

6. TypeError: Cannot read property ‘length’

このエラーは、Chromeでnullまたはundefinedのオブジェクトの長さを読み取ろうとしたときに表示されます。この問題の原因は、これまでの問題と同様ですが、配列を処理する際に頻繁に発生するため、特筆に値します。この問題は、以下のようなコードで確認できます。

未定義のオブジェクトを使用したTypeErrorの例
未定義のオブジェクトを使用したTypeErrorの例

しかし、新しいバージョンのChromeでは、このエラーはUncaught TypeError: Cannot read properties of undefinedとして表示されます。以下に例を示します。

未定義のオブジェクトを使用したTypeErrorの例(新しいバージョンのChromeで表示)
未定義のオブジェクトを使用したTypeErrorの例(新しいバージョンのChromeで表示)

この場合も、アクセスしようとしているオブジェクトが存在し、NULLに設定されていないことを確認するのが解決策となります。

7. TypeError: ‘undefined’ is not a function

このエラーは、スクリプト内に存在しないメソッドを呼び出そうとしたとき、または存在するものの、呼び出し側のコンテキストで参照できないときに表示されます。このエラーは通常Google Chromeで見られ、エラーの原因となっているコードの箇所を確認することで解決できます。もしタイプミスを見つけたら、それを修正し、問題が解決するかどうか確認してください。

コード内で(自己参照型の変数である)thisを使用した場合の注意点です。thisがコンテキストに適切に結びついていないと、このエラーが発生することがあります。次のようなコードを考えてみましょう。

function showAlert() {
    alert("message here")
}

document.addEventListener("click", () => {
    this.showAlert();
})

上記のコードを実行すると、説明したようなエラーが発生します。これは、イベントリスナーとして渡された無名関数が、documentのコンテキストで実行されているために起こります。

一方、関数showAlertwindowのコンテキストで定義されます。

これを解決するにはbind()メソッドで関数を紐付けることで、適切に参照できるようにする必要があります。

document.addEventListener("click", this.showAlert.bind(this))

8. ReferenceError: event is not defined

このエラーは、呼び出し元のスコープで定義されていない参照にアクセスしようとしたときに表示されます。イベント処理の際、コールバック関数の中でeventという参照を利用することが多いので、このようなことが起こります。このエラーは、関数のパラメータについてeventという引数を定義し忘れたり、スペルを間違えたりした場合に表示されることがあります。

このエラーは、Internet ExplorerやGoogle Chromeでは発生しないはずですが(IEにはグローバルイベント変数があり、Chromeはイベント変数を自動的にハンドラに紐付けるため)、Firefoxでは発生する可能性があります。ですので、このような小さなミスに注意することをお勧めします。

9. TypeError: Assignment to constant variable

これは不注意から起こるエラーです。定数に別の値を代入しようとすると、このような結果になります。

定数オブジェクトの割り当てに関連したTypeErrorの例
定数オブジェクトの割り当てに関連したTypeErrorの例

修正は一見すると簡単なようですが、何百ものこのような変数宣言があり、そのうちのひとつが誤ってletではなくconstと定義されているかもしれません。PHPなどの他のスクリプト言語と違い、JavaScriptでは定数と変数の宣言のスタイルにほとんど違いがありません。そのため、このエラーに直面した場合は、まず宣言を確認することをお勧めします。また、参照先が定数であることを忘れて変数として使ってしまった場合にも、このエラーが表示される可能性があります。これは不注意か、アプリのロジックに欠陥があることを示しています。これを重点的に意識して確認しましょう。

10. (unknown): Script error

スクリプトエラーは、サードパーティのスクリプトからブラウザにエラーが送信されたことを意味します。サードパーティスクリプトは、個々に開発中のアプリとは異なるドメインに属しているため、このエラーには「(unknown)」が付されます。サードパーティのスクリプトから機密情報が漏れるのを防ぐために、ブラウザでのその他の詳しい情報は非表示になります。

このエラーは、詳細がわからないと解決できません。そこで、このエラーに関する詳しい情報を獲得するためにできることをご紹介します。

  1. scriptタグにcrossorigin属性を付与する
  2. scriptをホストしているサーバーに正しいAccess-Control-Allow-Originヘッダを設定する
  3. (任意)サーバーにアクセスできない場合、プロキシを使用してリクエストをサーバーにつなぎ、正しいヘッダの付された状態でクライアントに返すことも可能

エラーの詳しい情報がわかったら、サードパーティ製ライブラリかネットワークのどちらかの問題に対して、具体的な策を講じてください。

JavaScriptのエラーの特定と防止方法

上で説明したエラーは、JavaScriptでよく見られるものですが、いくつかの例を知っておくだけでは十分ではありません。JavaScriptアプリケーションを開発する上で、どのようにエラーを検出し、防止するかを理解しておくことが重要です。そこで、JavaScriptのエラーを処理する方法をご紹介します。

手動でエラーを「throw」&「catch」する

手動で、またはランタイムによって返されたエラーを処理する最も基本的な方法が、catchです。他の多くの言語と同様に、JavaScriptにはエラーの処理に使える機能がいくつもあります。JavaScriptアプリケーションでのエラー処理について話す前に、それぞれを使ってできることを理解しておきましょう。

throw

最初に扱うのが、最も基本的なthrowです。名前からも明らかなように、throwはJavaScriptで例外を投げるのに使用します。これについては、すでに触れたとおりですが、もう少し詳しく役割を考えてみましょう。

  • 数値、文字列、Errorオブジェクトなど、何でもthrowすることができる
  • しかし、文字列や数値などのプリミティブ型は、エラーに関するデバッグ情報を持たないため、投げるのは得策ではない
  • 例:throw TypeError("Please provide a string")

try

tryは、コードが例外を投げる可能性があることを示すために使用されます。その構文は次のとおりです。

try {
    // error-prone code here
}

注意すべき点として、エラーを処理するために、必ずtryブロックの後にcatchブロックを記述するようにしましょう。

catch

catch は、ブロックをキャッチするのに使用します。つまり、tryブロックにより捕捉されたエラーを処理する役割を果たします。以下はその構文です。

catch (exception) {
    // code to handle the exception here
}

そして、trycatchをあわせて使用すると以下のようになります。

try {
    // business logic code
} catch (exception) {
    // error handling code
}

C++やJavaとは異なり、JavaScriptでは1つのtryブロックに複数のcatchブロックを付加することはできません。つまり、以下のようなことはできません。

try {
    // business logic code
} catch (exception) {
    if (exception instanceof TypeError) {
        // do something
    }
} catch (exception) {
    if (exception instanceof RangeError) {
    // do something
    }
}

代わりに、1つのcatchブロックの中でif...else文やswitch-case文を使って、起こりうるすべてのエラーのパターンを処理することができます。例えば、次のようになります。

try {
    // business logic code
} catch (exception) {
    if (exception instanceof TypeError) {
        // do something
    } else if (exception instanceof RangeError) {
        // do something else
    }
}

finally

finallyは、エラー処理後に実行されるコードブロックの定義に使用します。つまり、tryとcatchの後に実行されます。

また、finallyブロックは他の2つのブロックの結果に関係なく実行されます。つまり、catchブロックがエラーを完全に処理できない場合や、catch ブロックでエラーが発生した場合でも、プログラムがクラッシュする前にインタプリタによりfinallyブロックのコードが実行されます。

JavaScriptのtryブロックは、その後にcatchかfinallyどちらかが続かないと有効とは言えません。どちらも存在しないと、インタプリタによりSyntaxErrorが返されます。したがって、エラー処理を行う際には、tryブロックの後に少なくともどちらかのブロックを記述するようにしてください。

onerror() メソッドでグローバルにエラーを処理する

onerror()メソッドは、すべてのHTML要素で発生する可能性のあるエラーを処理するのに使用できます。例えば、imgタグが指定されたURLの画像を見つけられなかった場合、onerrorメソッドを呼び出します。こうすることで、ユーザーによるエラーの処理が可能になります。

通常、imgタグがフォールバックできるように、onerrorで別の画像URLを用意します。JavaScriptでこれを行う方法は次の通りです。

const image = document.querySelector("img")

image.onerror = (event) => {
    console.log("Error occurred: " + event)
}

この機能を利用して、アプリのグローバルなエラー処理の仕組みを作成することができます。以下が、その具体的な方法です。

window.onerror = (event) => {
    console.log("Error occurred: " + event)
}

このイベントハンドラを使用すると、コード内にある複数のtry...catchブロックを取り除き、イベント処理と同様にアプリのエラー処理を一元化することができます。「SOLID」(設計指針)の「単一責任の原則」を守るために、複数のエラーハンドラをwindowに紐付けることが可能です。該当するハンドラに到達するまで、インタプリタがハンドラを順に辿っていきます。

コールバックでエラーを渡す

直線的な関数では、シンプルにエラー処理が実行されますが、コールバックを用いることで、その手順を複雑化することができます。

次のようなコードを考えてみましょう。

const calculateCube = (number, callback) => {
    setTimeout(() => {
        const cube = number * number * number
        callback(cube)
    }, 1000)
}

const callback = result => console.log(result)

calculateCube(4, callback)

上記の関数では、処理に時間を取り、コールバックを利用することで後から結果を返すという非同期の仕組みが作られています。

関数呼び出しの際に4ではなく文字列を入力すると、結果としてNaNが返されます。

これに対処する方法が以下の通りです。

const calculateCube = (number, callback) => {

    setTimeout(() => {
        if (typeof number !== "number")
            throw new Error("Numeric argument is expected")

        const cube = number * number * number
        callback(cube)
    }, 1000)
}

const callback = result => console.log(result)

try {
    calculateCube(4, callback)
} catch (e) { console.log(e) }

これで理想的には問題が解決するはずです。しかし、関数呼び出しに文字列を渡そうとすると、次のようになります。

引数を間違えたエラーの例
引数を間違えたエラーの例

関数呼び出し、try-catchブロックを実装しているにもかかわらず、エラーが「uncaught」であると表示されます。タイムアウトによる遅延のため、catchブロックが実行された後にエラーが投げられます。

これは、ネットワーク通話で(予期せぬ遅延が介在し得るため)発生することがあり、このような状況を網羅しながら、アプリを開発する必要があります。

コールバックでエラーを処理する方法は、次の通りです。

const calculateCube = (number, callback) => {

    setTimeout(() => {
        if (typeof number !== "number") {
            callback(new TypeError("Numeric argument is expected"))
            return
        }
        const cube = number * number * number
        callback(null, cube)
    }, 2000)
}

const callback = (error, result) => {
    if (error !== null) {
        console.log(error)
        return
    }
    console.log(result)
}

try {
    calculateCube('hey', callback)
} catch (e) {
    console.log(e)
}

これで、コンソールの出力は、次のようになります。

誤った引数を含むTypeErrorの例
誤った引数を含むTypeErrorの例

これは、エラーが適切に処理されたことを意味します。

promise中のエラーの処理

非同期アクティビティの処理にはpromiseが好まれる傾向にあります。promiseには、それがrejectされてもスクリプトが終了することはない、という強みがあります。しかし、promiseのエラーを処理するために、catchブロックを実装する必要があります。理解を深めるために、promiseを使用してcalculateCube()関数を書き換えてみましょう。

const delay = ms => new Promise(res => setTimeout(res, ms));

const calculateCube = async (number) => {
    if (typeof number !== "number")
        throw Error("Numeric argument is expected")
    await delay(5000)
    const cube = number * number * number
    return cube
}

try {
    calculateCube(4).then(r => console.log(r))
} catch (e) { console.log(e) }

前のコードにあったtimeoutは、理解しやすいようにdelay関数に分離しました。4の代わりに文字列を入力しようとすると、出力は以下のようになります。

promiseの引数に誤りがある場合のTypeErrorの例
promiseの引数に誤りがある場合のTypeErrorの例

ここでも、他のすべての処理が完了した後にpromiseがエラーを投げることが原因となっています。この問題の解決方法は簡単です。以下のようにpromiseチェーンにcatch()呼び出しを追加するだけでOKです。

calculateCube("hey")
.then(r => console.log(r))
.catch(e => console.log(e))

これで、出力は以下のようになります。

誤った引数を利用したことによるTypeErrorの対処例
誤った引数を利用したことによるTypeErrorの対処例

promiseを用いたエラー処理がいかに簡単であるかをご理解いただけたはずです。さらに、finally()ブロックとpromise呼び出しを連鎖させ、エラー処理完了後に実行するコードを追加することもできます。

また、従来型のtry-catch-finallyを用いて、promiseのエラーを処理することもできます。その場合のpromise呼び出しは以下のようになります。

try {
    let result = await calculateCube("hey")
    console.log(result)
} catch (e) {
    console.log(e)
} finally {
    console.log('Finally executed")
}

しかし、これは非同期関数の中でのみ機能します。というわけで、promiseのエラーを処理する最適解は、promise呼び出しに、catchfinallyを連鎖させることです。

「throw/catch」、「onerror()」、「callback(コールバック)」、「promise」─どれを使うべきか

ここまでで触れてきたとおり、4つの方式があります。状況ごとに使える選択肢は異なります。続いては、選別方法を考えてみましょう。

throw/catch

ほとんどの状況で、これを使用することになります。catchブロックの中で、起こりうるすべてのエラーに対する処理を実装するようにしてください。また、tryブロックの後にメモリのクリーンアップを行う必要がある場合は、finallyブロックの記述をお忘れなく。

とは言え、try/catchブロックが多すぎると、コードのメンテナンスが難しくなります。そのような場合は、グローバルハンドラやpromiseメソッドでエラーを処理することをおすすめします。

非同期try/catchブロックとpromiseのcatch()のどちらを選択するについてですが、非同期try/catchブロックの方が、コードが直線的になり、デバッグしやすいので、そちらを推奨します。

onerror()

onerror() メソッドは、アプリケーションで多くのエラーを処理する必要があり、 しかも問題の箇所がコードベース全体に散らばっていることが分かっている場合に有用です。Onerror()を使用すると、エラーをアプリケーション内のイベントのように扱うことができます。複数のエラーハンドラを定義して、最初のレンダリング時にアプリのwindowに紐付けることが可能です。

しかし、エラーの範囲が狭い小規模なプロジェクトでは、onerror()メソッドの設定が不必要に難しくなることがありますのでご注意ください。対象のアプリにあまり多くのエラーがないことを確信しているなら、より基本的なthrow/catchを使う方がいいでしょう。

callback(コールバック)とpromise

コールバックとpromiseのエラー処理は、その設計や構造から異なります。コードを書く前にこの2つのどちらかを選ぶのであれば、promiseを選ぶのが賢明です。

というのも、promiseにはcatch()finally()ブロックを連鎖させてエラーを簡単に処理する仕組みが組み込まれています。この方法は、エラーを処理するために引数を定義したり、既存の引数を再利用したりするよりも簡単でクリーンです。

Gitリポジトリで変更点を追跡する

コードベース内のミスでエラーが発生することは、ある意味で不可避です。開発中やデバッグ中に、不必要な変更を加えてしまい、それが原因で新たなエラーが発生することすらあります。変更を加えるたびに自動でのテストを実行しておくのが得策です。ただし、これにより分かるのは、問題があるかないかという結果だけです。コードのバックアップを頻繁に取らないと、以前は問題なく機能していたはずの関数やスクリプトを修正することに、多くの時間を取られる可能性もあります。

そこで、Gitです。ちゃんとコミット戦略を立てることで、Gitの履歴をバックアップシステムとして使用し、開発中のコードの変遷を追跡することができます。以前のコミットの確認も簡単です。無駄に手を加えてしまった箇所や特定のバージョンを楽々見つけることができます。

そして、前のコードを復元したり、2つのバージョンを比較したりして、何が悪かったのかを判断することも可能です。GitHub DesktopやGitKrakenのような最新のウェブ開発ツールを使えば、変更点を並べて視覚化し、間違いを素早く突き止めることができます。

ミスを減らすための習慣として、コードに大きな変更を加えるたびにコードレビューを実施することをおすすめします。チームで作業している場合は、プルリクエストを作成し、チームメンバーに徹底的にレビューしてもらうのが鉄則です。あなたがうっかり見落としているエラーが見つかるかもしれません。

JavaScriptのエラーに関するベストプラクティス

ここまでは、JavaScriptアプリケーションでありがちなエラーを解決する方法を個別にご紹介してきました。対処法の把握には十分でしょう。とは言え、エラー解決の効果を最大化するためには、実装の際にいくつかの点に留意したいところです。具体的なヒントを以下にまとめました。

1. 操作上の例外処理に独自のエラータイプを使用する

記事の前半で独自のエラータイプの作成方法をご紹介しました。これは、アプリケーション固有の状況に合わせてエラー処理のしかたを調整できるようになるためです。一般的な Error クラスではなく、可能な限り独自のエラータイプを使用することをお勧めします。そうすることで、通常よりも多くの情報や背景が確認できるようになります。

さらに、独自のエラータイプでは、呼び出し側の環境におけるエラーの表示方法を調整することができます。つまり、特定の項目を隠したり、エラーに関するもっと詳しい情報を表示したりできます。

エラーの内容を好きなようにフォーマットすることも可能です。エラーの解釈や処理のしかたを細かく制御することができます。

2. 例外を放置しない

上級開発者であっても、コードの奥深くで例外をほったらかしにしてしまう、という初歩的なミスを犯すことがよくあります。

例えば、こんな状況です。実行が任意であるコードがあったとします。もしそれが機能すれば、素晴らしいことです。一方でもし機能しなくても、大した問題はありません。

このような場合、このコードをtryブロックに入れ、空のcatchブロックでその場しのぎをしたくなるかもしれません。しかし、そうすることで、そのコードがあらゆる種類のエラーを引き起こす可能性があります。これは、コードベースが大きくなった時、そして、このようなエラー管理の不備が多い場合、大きなリスクとなります。

例外を処理する上でのベストプラクティスはこうです。すべての例外を処理するレベルを決定し、そこに至るまでに、例外処理にとばすこと。このレベルは、コントローラ(MVCアーキテクチャのアプリの場合)でもミドルウェア(従来のサーバー指向のアプリの場合)でもかまいません。

こうすることで、アプリで発生しているすべてのエラーの場所を把握しながら対処法(たとえ何もしないことを選ぼうとも)を賢く選択できるようになります。

3. ログとエラーアラートを一元化する

エラーの処理における大事な要素として、ログを取ることが挙げられます。エラーのログを一元的に管理しないと、アプリの使用状況に関する貴重な情報を見逃してしまう可能性があります。

アプリのイベントログを確認することで、エラーに関する重要なデータを把握し、素早いデバッグに役立てることができます。アプリに適切なアラートメカニズムを設定しておいて、エラーが発生したときに、それが多くのユーザーに影響を及ぼす前に把握するのが理想です。

デフォルトのロガーを使用するか、好みにあわせてロガーを作成することをお勧めします。ロガーの中には、エラーのレベル (警告、デバッグ、情報など) に応じて処理方法を設定でき、中にはログをすぐにリモートロギングのサーバーに送信するものさえあります。このようにして、アプリケーションのロジックが利用者に対してどのように実行されるかを監視できます。

4. ユーザーに対し適切にエラーを通知する

エラー処理方法を定義する際に、ユーザーを念頭に置く必要があります。

アプリの正常な機能を妨げるすべての問題は、目に見えるかたちで(アラートやエラーメッセージで)確認できるようにしましょう。これにより、ユーザーによる解決策の実施が促せます。操作の再試行やログアウトと再ログインなど、エラーの簡単な解決方法を知っている場合は、アラートに必ず記載しましょう。

日常的な使用に支障をきたさないエラーの場合、アラートは出さずに、後で解決できるように、リモートサーバーで記録することも可能です。

5. ミドルウェアの実装(Node.js)

Node.js環境はサーバーアプリケーションに機能を追加するミドルウェアをサポートしています。この機能を利用して、サーバーのエラー処理用ミドルウェアを作成することができます。

ミドルウェアを使用する最も大きな利点は、すべてのエラーを一箇所で集中的に処理できること。この設定は、テスト用に簡単に有効/無効化できます。

基本的なミドルウェアの作り方を説明します。

const logError = err => {
    console.log("ERROR: " + String(err))
}

const errorLoggerMiddleware = (err, req, res, next) => {
    logError(err)
    next(err)
}

const returnErrorMiddleware = (err, req, res, next) => {
    res.status(err.statusCode || 500)
       .send(err.message)
}

module.exports = {
    logError,
    errorLoggerMiddleware,
    returnErrorMiddleware
}

そして、このミドルウェアをアプリ内で次のように使用することができます。

const { errorLoggerMiddleware, returnErrorMiddleware } = require('./errorMiddleware')

app.use(errorLoggerMiddleware)

app.use(returnErrorMiddleware)

これで、ミドルウェアの内部で独自のロジックを定義し、適切にエラーを処理できます。もう、コードベース全体で、個別のエラー処理構造の実装に頭を悩ませる必要はありません。

6. 意図的にアプリを再起動する (Node.js)

Node.jsアプリがエラーに遭遇した際、必ずしも例外を投げるとは限らず、アプリを終了しようとする場合があります。このようなエラーとして、CPUの大量消費、メモリの肥大化、メモリリークなど、プログラミングのミスに起因する問題が挙げられます。これが発生した場合には、Node.jsのクラスタモードや、PM2のようなツールを使ってアプリをクラッシュさせ、再起動(グレースフル・リスタート)させるのが理想です。これにより、ユーザーの操作によってアプリがクラッシュしてユーザー体験が害されるのを回避できます。

7. 「uncaught」の例外を全て「catch」する (Node.js)

プログラミングを徹底しようとも、アプリで発生する可能性のあるすべてのエラーをカバーできているとは限りません。したがって、アプリで発生する「捕捉から漏れた」例外をすべてcatchするフォールバック戦略を実装することが肝要です。

その方法をご紹介します。

process.on('uncaughtException', error => {
    console.log("ERROR: " + String(error))
    // other handling mechanisms
})

また、発生したエラーが標準的な例外なのか、特別な操作エラーなのかを識別することもできます。その結果に基づいて、プロセスを終了し、再起動することで、予期せぬ動作を回避することが可能です。

8. 未処理の「reject」をすべて「catch」する(Node.js)

例外をすべてカバーすることができないのと同様に、promiseのrejectをすべて処理しきれない可能性もあります。しかし、例外とは異なり、promiseのrejectはエラーを投げません。

そのため、重要なpromiseがrejectになった場合、それが最終的には、予期せぬ動作につながるリスクがあります。そのため、promiseのrejectを処理するフォールバックの仕組みを実装することが非常に重要です。

その方法が、以下の通りです。

const promiseRejectionCallback = error => {
    console.log("PROMISE REJECTED: " + String(error))
}

process.on('unhandledRejection', callback)

まとめ

どのプログラミング言語でもエラーを完全に避けることはできません。JavaScriptも例外ではなく、エラーは自然なものです。場合によっては、ユーザーへの反応として、意図的にエラーを投げる必要があることさえあります。したがって、エラーの構造と種類を理解することは非常に重要です。

さらに、アプリケーションを停止させてしまうエラーを特定し、それを防ぐための知識と道具を用意しておく必要があります。

多くの場合、JavaScriptアプリケーションの種類に限らず、慎重にエラーを処理していく戦略を構築することが鍵となります。

他にまだ解決できていないJavaScriptのエラーはありますか?JSのエラーを建設的に処理するテクニックがあれば教えてください。以下のコメント欄でお聞かせください。

Kumar Harsh

インドを拠点とするソフトウェア開発者、テクニカルライター。JavaScriptとDevOpsが専門。詳しい仕事情報は自身のウェブサイトで公開している。