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

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

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

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

[^1]: https://developer.salesforce.com/jpblogs/2020/02/salesforce-developer-blog-campaign-2020/

# 公式情報

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

* [Handle Errors in Lightning Data Service - Salesforce Lightning Component Library](https://developer.salesforce.com/docs/component-library/documentation/en/48.0/lwc/lwc.data_error)

> * 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コンポーネント](https://github.com/trailheadapps/lwc-recipes/tree/f988d640eca8be4a7bb6dc3b9380494ea1cd3d61/force-app/main/default/lwc/errorPanel) を使っています。
このコンポーネントはエラーメッセージをつなげて箇条書きで表示するというコンポーネントです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27267/88311510-cfa9-e8a6-6b28-3bb8e7685711.png)

その中で使っている[reduceErrors](https://github.com/trailheadapps/lwc-recipes/blob/23c6e42ce826d8b230005392829308bebf542698/force-app/main/default/lwc/ldsUtils/ldsUtils.js#L6)を見るとどういう値が返ってくると想定しているかがある程度はわかりますが、人間向けのメッセージの取り方しかわかりません。例えば、Apexの例外が投げられた場合はスタックトレースなども返ってきますが、その情報は消えてしまいます。

# やりたいこと

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

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

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

[^2]: [Messaging Overview - Lightning Design System](https://www.lightningdesignsystem.com/guidelines/messaging/overview/)
[^3]: https://github.com/atskimura/lwc-recipes/blob/master/force-app/main/default/lwc/ldsCreateRecord/ldsCreateRecord.js#L34

# 作ったもの

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

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

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

**インライン版**

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27267/23e3685c-240d-a4de-2123-76825dcc1661.png)

**Toast版**

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27267/d63fe2d4-99e9-e850-af9c-00c0cd625adb.png)

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

# コード

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文は不要です。


```javascript:apexWireMethodToProperty.js
@wire(getContactList) contacts;
```

```html:apexWireMethodToProperty.html
<c-error-panel errors={contacts.error}></c-error-panel>
```

## error-toastコンポーネント

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

```javascript: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;
            });
    }
```

```html:ldsCreateRecord.html
<c-error-toast errors={error} friendly-message="作成に失敗しました。"></c-error-toast>
```

# ちょっとコード解説

詳細に表示するためにエラーオブジェクトをJSON文字列に変換するところだけ少し工夫しています。 `JSON.stringify` はJavaScriptのErrorオブジェクトのときに `{}` を返すので、[このstackoverflowのコメント](https://stackoverflow.com/a/53624454)のコードで対応しています。（JSON.stringifyの[replacer引数](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify)って知らなかった。）

```javascript
/**
 * エラーオブジェクトを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);
}
```

# 終わりに

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