Salesforce Developers Japan Blog

Lightning Web コンポーネントのエラー処理のベストプラクティス

(この投稿は Aditya Naag Topalli(Salesforce.com) による『Error Handling Best Practices for Lightning Web Components』の翻訳です)

エラー処理はどのようなアプリケーションにとても重要であり、設計段階からアプリに組み込むべきです。適切に定義されたエラー処理パターンとベストプラクティスを使用することで、アプリが予想されるエラーと予想外のエラーの両方を一貫して優雅に処理できるようになります。このブログでは、Lightning Web コンポーネントのエラー処理のベストプラクティスについて説明します。Lightning Web コンポーネントのフレームワークは標準ベースなので、ほとんどのエラー処理のベストプラクティスも標準に基づいています。

サーバーサイドのエラー処理

Lightning Web コンポーネントは、サーバー上のデータの存在に依存する UI フレームワークです。Lightning Web コンポーネントを使用して Salesforce のデータを操作する最も簡単な方法は、基本 Lightning コンポーネントLightning データサービスによって提供されるワイヤアダプタを使用することです。しかし、データをクライアントに返す前に、カスタムビジネスロジックを実行して複雑な変換を適用する必要がある場合もあり、このカスタムロジックは通常、Apex を使用して記述されます。

Apex のメソッドは、さまざまな例外を投げることができます。これらには、クエリの例外、DML の例外、およびパースや型変換などのビジネスロジックからの一般的な例外が含まれます。Apex コードで処理せずに例外を直接クライアントに送信させることもできますが、予めサーバー側でエラーを処理することで、どの種類のエラーをクライアントに表示するか、どの種類のエラーをバックエンドで処理するかを制御することができます。

エラーを処理する最良の方法は、try-catch ブロックの中でロジックをラップすることですが、リミット例外 (System.LimitException) のように、キャッチブロックでは処理できない種類のエラーもあります。フロントエンドに例外を投げるときは必ず、独自のカスタム例外クラスを作成して、エラーメッセージをカスタマイズしたり、クライアントに送信される例外の詳細を抽象化することをお勧めします。

try {
    // Perform logic that may throw an exception.
} catch (Exception e) {
    throw new CustomException(e.getMessage());
}

//Custom Exception class
public class CustomException extends Exception {

}

ベストプラクティスとして、エラーを送信するための共通の構造を決め、すべてのバックエンドクラスでそれに従うようにしてください。事前に定義されたエラータイプがあれば、クライアントに返す詳細を簡単に決めることができます。

クライアント側でのエラーの処理

クライアント側で発生する全てのエラーが同じように処理できるわけではありません。ここでは、Lightning Web コンポーネントのエラーの発生源によって、エラー処理の仕組みがどのように変わるのかを見ていきます。

try-catch ブロック

try-catch はコード内のエラーを処理する最も一般的な方法ですが、誤用される可能性があります。すべてのコードを try ブロック内に配置することは、ベストプラクティスとは考えられていません。try ブロック内に配置されるコードは、そのコードによって発生するエラーがどのように処理されるかに基づいていなければなりません。異なるコードブロックからのエラーを異なる方法で処理しなければならない場合は、複数の try-catch ブロックを使用してください。また、catch ブロックは、その中で発生する可能性があるエラーのみを処理し、残りはさらに伝搬させるべきです。

覚えておくべきことは、try-catch ブロックは同期コードの例外のみを処理できるということです。setTimeout や Promise のように、非同期コードの実行で例外が発生した場合、try-catch はそれをキャッチしません。以下の例では、エラーが非同期プロセス内で発生したため、キャッチブロックは実行されません。

try {
    setTimeout(() => {
        throw new Error('some error');
    }, 1000);
} catch (e) {
    console.error("An error occurred"); //This will not be executed
}

ワイヤ、命令的呼び出し、プロミスなどの非同期処理によるエラー

前述したように、非同期呼び出しのエラーは、try-catch ブロックの中で呼び出しをラップしてもキャッチできません。setTimeout や setInterval のような時間ベースのイベントの場合は、コールバック関数の内部で try-catch ブロックを使用してエラーを処理する必要があります。

setTimeout(() => {
    try {
        //logic
    } catch (e) {
        //handle error
    }
}, 300)

ワイヤード・メソッドの場合、潜在的なエラーの発生源が 2 ヶ所があります。それは、ワイヤアダプタによる値のプロビジョニング中と、プロビジョニングされた結果を処理するためのカスタムロジックの実行中です。値のプロビジョニングでエラーが発生すると、エラープロパティに自動的に格納されます。このプロパティを解析することで、エラーの原因を知ることができ、それに応じて処理することができます。データのプロビジョニングが正常に行われ、データを処理するロジックからエラーを処理したい場合は、以下の例のようにトライキャッチブロック内にロジックを配置します。

@wire(getContactList)
wiredContacts({ error, data }) {
    if (data) {
        try {
            //logic to handle result
        } catch(e){
            //error when handling result
        }
    } else if (error) {
        //error with value provisioning
    }
}


命令的な呼び出しや Promise を使用する場合、エラーを処理する方法は 2 つあります。

1.catch メソッドを使用して、プロミスチェーン全体で発生するエラーを処理します。これには、サーバーからのエラーと then メソッドに書かれたロジックからのエラーが含まれます。catch() メソッドを省略すると、then() メソッドからのエラーを破棄してしまいます。ベストプラクティスとして、すべてのプロミスに catch メソッドがあることを確認してください。下の例では、getContactList と then メソッドの両方で投げられたエラーを catch メソッドが処理しています。

getContactList()
    .then(result => {
        //logic to handle result... dont need a try catch
    })
    .catch(error => {
        //logic to handle errors
    });

2.非同期関数を呼び出すもう一つの方法に async/await パターンがあり、この場合は同期関数と同じようにコードを try-catch ブロックの中にラップします。

async getDetails(){
    try{
        let result = await getContactList();
    } catch(e){
        // Handle Errors
    }
}

コンポーネントのライフサイクルハンドラ内のエラー

コンポーネントのライフサイクルハンドラには、クラスの初期化などを行う constructor() の他、connectedCallback()、renderedCallback() などがあります。ライフサイクルハンドラ内で発生したエラーは、try-catch ブロックでラップすることができますが、プロパティ値を計算する際に発生したエラーは同じように処理することができません。

 
export default class PropertyInitErrorExample extends LightningElement {
    sum = 10;
    count = 0;
    avg = sum/count; //results in an exception
}

クラスのフィールド/プロパティの値はインラインで計算しないようにし、計算にはゲッターメソッドを使用して、必要に応じてメソッド内のロジックを try-catch ブロックでラップできるようにします。また、コンポーネントのライフサイクル内のエラーを処理するために、errorCallback() ハンドラを使用して境界コンポーネントとしての処理を記述することもできます。これについては、「エラーのライフサイクルと伝播」のセクションで詳しく説明します。

エラーの表示とロギング

さまざまな種類のエラーをキャッチする方法を見てきましたが、次は Lightning Web コンポーネントでこれらのエラーを表示する際のベストプラクティスを見てみましょう。

エラー本体のペイロードを理解する

エラーを表示する前に、様々なシナリオでキャッチするエラーオブジェクトの構成を理解しておくと便利です。

JavaScript や Web Platform API では、ReferenceError や TypeError などの reject エラータイプを投げます。これらはすべて、以下のプロパティを持つ Error オブジェクトを継承しています。

 

  • name – 投げられた例外のタイプ
  • message – エラーメッセージ
  • stack – スタック・トレース

 

以下にコードスニペットの例と、その結果のコンソール出力を示します。

try{
    undefinedVariable.toString();
} catch(e){
    console.error(e);
    console.error('e.name => ' + e.name );
    console.error('e.message => ' + e.message );
    console.error('e.stack => ' + e.stack );
}


Apex または Lightning データサービスを使用して Salesforce データにアクセスすると、Fetch API の Response オブジェクトをモデルにした上の例とは異なる構造のカスタムエラーオブジェクトを使用してエラーが表示されます。以下はカスタムエラーオブジェクトに含まれるプロパティの例です。

  • ok – リクエストの成否
  • status – レスポンスの HTTP ステータスコード。たとえば内部サーバー エラーの場合は 500。
  • statusText – ステータスコードに対応するステータスメッセージ
  • body – レスポンスボディ。エラーをスローしたメソッドに応じて異なる。

Apex によって投げられた補足されない例外とカスタム例外については、body プロパティで例外の種類 (body.exceptionType)、エラーメッセージ (body.message)、Apex スタックトレース (body.stackTrace) などの追加の詳細を確認できます。以下にコードスニペットの例とその結果のコンソール出力を示す。

//Apex Code
@AuraEnabled
public static Integer someMethod() {
    return 10/0;
}

//JavaScript Code
someMethod()
    .then(result => {
        //Handle Result
    })
    .catch(error => {
        console.error(error);
    });

サーバー上のレコードやオブジェクトなどのリソースにアクセスできない場合や、ワイヤアダプタに無効なオプション(無効なレコード ID や必須フィールドの欠落など)を渡した場合、または検証ルールが失敗した場合に、Lightning データサービスでエラーが発生します。Lightning データサービスは、Apex が返すものと非常によく似たカスタムエラーオブジェクトを返しますが、エラーの body プロパティの値は API によって異なります。このオブジェクトには単一のオブジェクトまたはオブジェクトの配列が含まれている場合があるため、ロジックはこれを解析する際に両方のデータ型をチェックする必要があります。以下に例を示します。

@wire(getRecord, { recordId: '$recordId', fields })
wiredRecord({error, data}) {
    if (error) {
        // UI API read operations return an array of objects
        if (Array.isArray(error.body)) {
            this.error = error.body.map(e => e.message).join(', ');
        } 
        // UI API write operations, Apex read and write operations 
        // and network errors return a single object
        else if (typeof error.body.message === 'string') {
            this.error = error.body.message;
        }
    } else if (data) {
        // Process record data
    }
}

ご覧になったように、エラーオブジェクトの構造はそれぞれのケースで異なります。各コンポーネントで異なる種類のエラーオブジェクトを解析するロジックを繰り返すのではなく、これを行う単一の関数を作成し、それを各コンポーネントのモジュールとしてインポートすることができます。

ベストプラクティスとして、LWC Recipes(Lightning Web コンポーネントのコード例集)にある reduceErrors 関数を使用して、異なる種類のエラーオブジェクトを処理します。この関数は、メッセージプロパティを抽出し、複数のメッセージプロパティが見つかった場合にメッセージを連結します。ここでは、先ほどのコードスニペットを簡略化するためにどのように使用できるかの例を紹介します。

import { reduceErrors } from 'c/utils';
...

@wire(getRecord, { recordId: '$recordId', fields })
wiredRecord({error, data}) {
    if (error) {
        this.errorMessage = reduceErrors(this.error);
    } else if (data) {
        // Process record data
    }
}

しかし、サードパーティのコードを扱う際には、エラーボディのペイロードが若干異なる場合があることを覚えておきましょう。JavaScript の throw 文は、数値や文字列を含む任意の式を投げることができます。そのため、サードパーティのコードからの例外を扱う際には、上で説明したアプローチでは不十分な場合があるので注意が必要です。

エラーの表示

エラーの表示はおそらくエラー処理メカニズムの中で最も重要な部分です。エラーはユーザに意味のある方法で表示されなければなりません。最も推奨される方法は、エラーが発生したポイントの近くでユーザーにエラーを表示することです。例えば、テキストフィールド。ボタンがクリックされたときにエラーが発生する場合や、エラーが複数の箇所で発生した場合は、トーストメッセージがより適切です。

基本 Lightning コンポーネントは、フォームにエラーメッセージを表示するための簡単で一貫性のある方法を提供します。基本 Lightning コンポーネントは、検証状態に応じてフォームコントロールやフォーム自体に CSS クラスを自動的に追加したり削除したりします。reportValidity メソッドと setCustomValidity メソッドを使用して、エラーメッセージをプログラムで制御することができます。

また、エラーメッセージはユーザーフレンドリーであるべきで、エラーが発生したことを示すだけでは役に立ちません。メッセージは、エラーが何であるか、そしてユーザーがそれを修正するために何ができるかを正確に示す必要があります。

一貫したエラー処理と表示メカニズムを持つためには、エラーを表示するための再利用可能なコンポーネントを作成し、すべてのコンポーネントでそれを使用することがベストプラクティスです。これは、LWC RecipeserrorPanel コンポーネントでも利用されています。また、このコンポーネントは先ほど説明した reduceErrors 関数を使用して、すべての形式のエラーオブジェクトを処理し、一貫したユーザーインターフェイスを表示します。以下に例を示します。

<template if:true={error}>
    <c-error-panel errors={error}></c-error-panel>
</template>

エラー・ロギング

ユーザーにエラーを表示するだけでなく、コンソールにエラーを出力することもできます。console.error() 関数を使用すると、元のエラーメッセージのコールスタックを記録しつつ、呼び出された console.error() のコールスタックも記録されるため、ベストプラクティスとして使用してください。これは、DevTools コンソールでメッセージの横にある矢印をクリックすると表示されます。

すべてのコンソール ロギング API は複数の引数を受け入れます。捕らえたエラーにさらに情報を追加する必要がある場合は、console.error(‘Unexpected error during some operation’, error) を使用します。

また、より良い追跡と報告の目的のために、可能な限りエラーはサーバーに記録します。これは、本番環境で問題をデバッグする際に最も有用です。アプリケーションの運用が開始されると、エンドユーザが明示的に共有しない限り、ログに記録された出力に開発者がアクセスできないため、 console.error に何の価値もありません。

エラーのライフサイクルと伝播

すべてのエラーが発生源での近くに表示できるわけではなく、また、すべてのエラーがユーザー・インターフェースを持つコンポーネントで発生するわけではありません(サービス・コンポーネントなど)。そのようなエラーを表示するためには、親コンポーネントに伝搬されなければなりません。未処理のエラーはデフォルトではコンポーネント階層を介して伝搬されることになります。

Lightning Web コンポーネントのエラーライフサイクル

コードでエラーが発生した場合、Javascript はエラーをキャッチするハンドラを探します。エラーの発生源にできるだけ近いところで処理するのがベストプラクティスです。Lightning Web コンポーネントの場合、処理されていないエラーは子コンポーネントから親コンポーネントに伝搬します。最上位の親コンポーネントがエラーを処理しない場合は、Lightning ランタイムに投げられます。

同期操作によるエラーは、エラーの行番号とスタックトレースを含む “Sorry to interrupt” ポップアップを表示することで、Lightning ランタイムが処理します。エラーはそれ以上ブラウザに伝わりません。ワイヤ関数やプロミスなどの非同期操作によるエラーの場合、エラーはブラウザに伝わり、ブラウザのコンソールに表示されます。またランタイムは、コンテキストに応じて画面にエラーを表示します。例えば、Lightning Experience で Lightning Web コンポネントを実行している場合、非同期エラーは UI に表示されません。しかし、フロー内でLightning Web コンポーネントを実行している場合、フローのランタイムは画面の下部にエラーを表示します。

エラーの伝播

上のライフサイクルで見たように、処理されていないエラーはデフォルトで伝搬します。しかし、上記のいずれかのエラー処理メカニズムを使用する場合、コンポーネントレベルでエラーを処理するか、他のコンポーネントに処理を任せるために伝搬させるかを選択することができます。エラーは、throw キーワードを使用するか、カスタムイベントを使用して伝搬させることができます。throw キーワードを使用すると、使用された時点で関数が停止しますが、カスタムイベントを使用すると、イベントを実行した後の処理を柔軟に決めることができます。throw キーワードを使用してスローされたエラーは、コンポーネントの親によってのみ処理できますが、カスタムイベントを使用すると、階層外のコンポーネントを使用して処理することもできます。

ベストプラクティスとして、低レベルのコンポーネント(サービス・コンポーネント、ユーティリティ関数など)からエラーを伝搬し、高レベルのコンポーネントでエラーを処理します。例外が上位レベルで処理される理由は、下位レベルではエラーを処理するための最も適切な挙動が何かがわからないからです。

ここでは、同じ関数に対し、エラーをスローするものとカスタムイベントを発生させるものの 2 つのバリエーションを示します。

export default class Hello extends LightningElement {

    //Using Throw keyword
    divide_with_throw(a, b){
        if(b == 0){
            throw new Error('Cannot divide by 0'); 
        }
        return a/b;
    }

    //Using Custom Events. You can also use Pubsub or Lightning Message Service.
    divide_with_event(a, b){
        if(b == 0){
            const selectedEvent = new CustomEvent('error', { detail:'Cannot divide by 0' });
            this.dispatchEvent(selectedEvent);
        } else {
            return a/b;
        }
    }
}

次のステップは、これらのエラーを親コンポーネントで処理することでしょう。

カスタムイベントを使用している場合は、それらのイベント用のイベントハンドラを書けばよいのです。カスタムイベントは実際にはエラーではないので、catch ブロックを使って捕捉できないことに注意してください。

カスタムエラーを投げる場合は、上記のエラー処理メカニズムを使用して個々のエラーをキャッチするか、errorCallback() ハンドラを使用して未処理のエラーとカスタムエラーをすべて捕捉することができます。

errorCallback() は、そのツリー内のすべての子孫コンポーネントからのエラーをキャプチャするライフサイクルハンドラです。子孫のライフサイクルハンドラや、コンポーネントの HTML テンプレートで宣言されたイベントハンドラ内で発生したエラーを捕捉します。errorCallback()フックについて覚えておくべきことがいくつかあります。プログラムで追加されたイベントハンドラ(addEventListener など)は捕捉されません。エラーがキャッチされると、フレームワークは DOM からエラーを発生させた子コンポーネントをアンマウントします。子孫コンポーネントで発生したエラーはキャッチしますが、自身のコンポーネントで発生したエラーはキャッチしません。

ベストプラクティスとして、errorCallback() を実装する境界コンポーネントを作成し、その中に機能コンポーネントを埋め込みます。ここでは、エラーが errorCallback() によりキャッチされ、errorPanel 境界コンポーネントを使用して表示する例を示しています (先ほど説明した reduceErrors 関数を使用しています)。

<template>
    <template if:true={this.error}>
        <c-error-panel errors={this.error}></c-error-panel>
    </template>
    <template if:false={this.error}>
        <!-- YOUR COMPONENT -->
    </template>
</template>


import { LightningElement } from 'lwc';

export default class Boundary extends LightningElement {
    errorCallback(error, stack) {
        this.error = error;
    }
}

正しいバランスを見つける

すべてのエラーがコンポーネント内で処理される必要があるわけではないことを覚えておくことが重要です (低レベルのコンポーネントのエラーなど)。エラーを捕捉せずに放置しておくと、エラーの根本原因を特定して修正するのが簡単になることもあります。これは、開発やテストの段階で特に有用です。しかし、エラーをキャッチするタイミングとキャッチしないタイミングの適切なバランスを見つけるのは難しいかもしれません。ここではそのヒントを紹介します。

  • アプリケーションを失敗させることは、エラーをうまく処理できないよりも、常に望ましいことです。
  • 外部やサードパーティのコードを扱う際には、常にエラーを適切に処理するようにしてください。
  • サーバへの呼び出し、サードパーティのライブラリ、外部サービスへの呼び出しなど、アプリケーションの境界点でのエラーを常に処理するようにしてください。
  • 必要に応じて自分のコードでエラーを投げることを恐れないでください。

まとめ

このブログでは、コードのどの部分で発生するかによって、エラーを処理する方法が異なることを見てきました。また、エラーオブジェクトの異なるフォーマットと reduceErrors 関数がエラーメッセージの抽出にどのように役立つか、境界コンポーネントを作成することで未処理のエラーからコンポーネントツリーを保護する方法についても見てきました。最後に、イベントと throw 文を使用してコンポーネント階層にエラーメッセージを伝搬させる方法を紹介しました。サンプルギャラリーでは、ベストプラクティスを実践しているアプリを見ることができます。

さらに学習を進めるために、いくつかの追加リソースをご紹介します。

著者について

Aditya Naag Topalli は、Salesforce リードデベロッパーエバンジェリストです。彼は、Lightning Web コンポーネント、Einstein Platform Services、およびインテグレーションにフォーカスしています。技術的なコンテンツを執筆し、世界中のウェビナーやカンファレンスで頻繁に講演を行っています。

Twitter @adityanaag 

コメント

Lightning Web コンポーネントのエラー処理のベストプラクティス