LWCのエラーハンドリングのベストプラクティスを考える


LWC(Lightning Web Component)のコードを書いていて、悩むのがエラーハンドリングです。ドキュメントのどこにもエラーレスポンスの定義が明確に記載されていませんし、場合によって返ってくるレスポンスがまちまちです。

この記事ではLWCのエラーハンドリングのベストプラクティスを考えてみたいと思います。

なお、この記事も前の記事に引き続きSalesforce 開発者向けブログキャンペーンへのエントリー記事です。1きっかけがないと記事が書けない人間です

公式情報

ドキュメントには以下の記述しか見つかりません。

  • UI API read operations, such as the getRecord wire adapter, return error.body as an array of objects.
  • UI API write operations, such as the createRecord wire adapter, return error.body as an object, often with object-level and field-level errors.
  • Apex read and write operations return error.body as an object.
  • Network errors, such as an offline error, return error.body as an object.

書いてあるのはこれだけです。objectって中身の定義は何なのでしょう?

ドキュメントのサンプルコートではだいたい <c-error-panel errors={contacts.error}></c-error-panel> のように書かれていて、lwc-recipesの error-panelコンポーネント を使っています。
このコンポーネントはエラーメッセージをつなげて箇条書きで表示するというコンポーネントです。

その中で使っているreduceErrorsを見るとどういう値が返ってくると想定しているかがある程度はわかりますが、人間向けのメッセージの取り方しかわかりません。例えば、Apexの例外が投げられた場合はスタックトレースなども返ってきますが、その情報は消えてしまいます。

やりたいこと

ISVベンダーとしては、ユーザからのエラー報告時点である程度原因の当たりをつけたいです。ApexのスタックトレースやJavaScript Errorのスタックトレースがその時点でもらえると原因究明にかなり役立ちます。

SalesforceネイティブなAppExchangeアプリでは一般的なSaaSとは異なりサーバーログをベンダーが自由に見ることはできません。ユーザにログインアクセスを許可してもらえばApexのログを見れますが、許可をもらうのに多少のハードルがあるし、時間もかかってしまうし、再現させないと見ることができません。ログインアクセス許可をもらわずとも当たりがつく方がトラブルを速く解消できます。

またいつもインライン表示というのは使いにくいです。初期表示のエラーはインライン表示でいいですが、ユーザ操作時のエラーはToastで表示したいです。SLDSのデザインガイドラインでもそう言ってますし2、実際lwc-recipesでも保存時のエラーなどはToastで表示しています。3

作ったもの

というわけで、エラーレスポンス定義は正確にはわからないこともあり、以下の方針でlwc-recipesのerror-panelコンポーネントを改造してみました。

  • メッセージ取得までは公式サンプルのreduceErrorsに任せる(きっと全パターンに対応してくれてるだろう)。
  • より詳細なエラーはそのままJSONに変換して表示する。「Show Details」ではそれを表示する。
  • Toast版も作成する。

完成品は以下のようになります。

インライン版

Toast版

これであれば、ユーザに「詳細を表示」をクリックして表示されたテキストをコピーして送ってもらえば、だいぶ原因の当たりがつくでしょう。

コード

lwc-recipesをforkしてブランチで修正しました。
https://github.com/atskimura/lwc-recipes/tree/error-handling

変更箇所は以下の通りです。
https://github.com/atskimura/lwc-recipes/compare/master...error-handling

使い方

(改造)error-panelコンポーネント

既存のerror-panelコンポーネントと基本的に同じです。ただし、中で <template if:true={errors}> のチェックするようにしたので使う側ではif文は不要です。

apexWireMethodToProperty.js
@wire(getContactList) contacts;
apexWireMethodToProperty.html
<c-error-panel errors={contacts.error}></c-error-panel>

error-toastコンポーネント

Toast表示のコンポーネントです。使い方は同じで、LDSでもApexでもwireでも返ってきたエラーオブジェクトを <c-error-toast errors={error}> に渡せばよいです。

ldsCreateRecord.js
    createAccount() {
        const fields = {};
        fields[NAME_FIELD.fieldApiName] = this.name;
        const recordInput = { apiName: ACCOUNT_OBJECT.objectApiName, fields};
        createRecord(recordInput)
            .then(account => {
                this.accountId = account.id;
                this.dispatchEvent(
                    new ShowToastEvent({
                        title: 'Success',
                        message: 'Account created',
                        variant: 'success'
                    })
                );
            })
            .catch(error => {
                this.error = error;
            });
    }
ldsCreateRecord.html
<c-error-toast errors={error} friendly-message="作成に失敗しました。"></c-error-toast>

ちょっとコード解説

詳細に表示するためにエラーオブジェクトをJSON文字列に変換するところだけ少し工夫しています。 JSON.stringify はJavaScriptのErrorオブジェクトのときに {} を返すので、このstackoverflowのコメントのコードで対応しています。(JSON.stringifyのreplacer引数って知らなかった。)

/**
 * エラーオブジェクトをJSON文字列に変換する。
 * https://stackoverflow.com/a/53624454
 * 
 * @param {FetchResponse|FetchResponse[]} errors
 * @return {String} JSON String
 */
export function getErrorDetails(errors) {
    function jsonFriendlyErrorReplacer(key, value) {
        if (value instanceof Error) {
            return {
                // Pull all enumerable properties, supporting properties on custom Errors
                ...value,
                // Explicitly pull Error's non-enumerable properties
                name: value.name,
                message: value.message,
                stack: value.stack,
            }
        }
        return value
    }

    return JSON.stringify(errors, jsonFriendlyErrorReplacer, 2);
}

終わりに

けっこう便利じゃないですかね。