Blazor <ErrorBoundary>をカスタマイズしてGlobal エラーハンドリング(.Net 8)

## １．この記事の目的
Blazorでは```<ErrorBoundary>```で効率よくGlobalなエラーハンドリングが実現できます。
また、簡単なカスタマイズでログ出力やエラーページのレイアウト変更など可能なので整理しました。  


## ２．前提条件
- Visual studio 2022 Version 17.12.3
- .Net 8
- UI：Blazor Web App
- Interactive render mode : Server  
- Interactivity location ： Global
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546646/e3f964f4-af67-d85e-d014-13b8df9ff447.png)
:::note alert
Interactivity location を Global から Per Pageにすると```<ErrorBoundary>```でのグローバルエラーハンドリングが複雑になるためここでは考えません。
:::


## ３．```<ErrorBoundary>```

### ３．１ ```<ErrorBoundary>```の実装

構文
```html
<ErrorBoundary>
  <ChildContent>
     {正常時の表示コンテント}
  </ChildContent>
  <ErrorContent>
     {異常キャッチ時の表示コンテント}
  </ErrorContent>
</ErrorBoundary>
```

MainLayout.razorにて```@Body```を```<ErrorBoundary>```で囲みます。
```html
<article class="content px-4">
    @Body
</article>
```
  ↓ 変更

```html
<article class="content px-4">
    <ErrorBoundary>
        <!-- 正常系 -->
        <ChildContent>
            @Body
        </ChildContent>
        <!-- エラー系 -->
        <ErrorContent>
            <p class="text-danger">システムエラーが発生</p>
            <div class="mt-3">
                <a href="" @onclick="NavigateToHome">Homeへ</a>
            </div>
        </ErrorContent>
    </ErrorBoundary>
</article>
```



### ３．２ エラー発生タイミングごとの挙動
InteractiveServerレンダーモードではエラー発生タイミングが大きく2種類存在します。

|  | タイミング  | 概要  |
|:-:|:-|:-|
| A | HTTPリクエストの応答中 | GETなどのページ取得時等 |
| B | SignalR通信中 | ボタンクリックイベントなど初期ページ表示後のイベント等 |
 

#### (A) HTTPリクエストの応答中のエラー
HTTPリクエスト応答中にOnInitializedでエラーを発生させます。

Weather.razor
```html
@page "/weather"

<PageTitle>Weather</PageTitle>
<h1>Weather</h1>
<p>HTTPリクエスト時に強制エラー</p>
@code {
    protected override void OnInitialized()
    {
        throw new Exception("OnInitializedで強制エラー発生");
    }
}
```

挙動
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546646/934945b3-8596-b35a-2140-36cbb23b12db.png)

エラーがハンドリングされ、```<ErrorContent>```で用意したコンテントが表示されます。


#### (B) SignalR通信中のエラー

Counter.razor
```html
@page "/counter"

<PageTitle>Counter</PageTitle>
<h1>Counter</h1>
<p role="status">Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
    private int currentCount = 0;
    private void IncrementCount()
    {
        currentCount++;
        if (currentCount > 3)
        {
            throw new Exception("counter IncrementCount");
        }
    }
}
```

挙動
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546646/bd8f7a0a-49ca-5168-5f90-64d8e0e55ccf.png)

同じです。
エラーがハンドリングされ、```<ErrorContent>```で用意したコンテントが表示されます。

## ４．デフォルトのログ出力

### ４．１ コンソール出力
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546646/9237f0dd-9798-ef09-0ebd-8e65aeb6ac0f.png)
以下が確認できる。
- ErrorBoundryがExceptionをキャッチしたこと
- ThrowしたExceptionのメッセージ「OnInitializedで強制エラー発生」
- エラー発生時のスタックトレース


### ４．２ Windowsイベントログ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546646/68089407-34cb-eb2c-05e5-bb37c9fb49fa.png)
同様の内容が確認できる。

## ５．ログ出力の強化
```<ErrorBoundary>```を継承し、カスタマイズします
```OnErrorAsync```を```override```し、エラー発生時に好みの処理を記述します

/Layout/CustomErrorBoundary.razor
```C#
@inherits ErrorBoundary
@inject ILogger<CustomErrorBoundary> Logger

@if (CurrentException is null)
{
    @ChildContent
}
else if (ErrorContent is not null)
{
    @ErrorContent(CurrentException)
}

@code {
    protected override Task OnErrorAsync(Exception ex)
    {
        Logger.LogError(ex, "共通エラーハンドリング");
        return Task.CompletedTask;
    }
}
```

MainLayout.razor
```html
<ErrorBoundary>
      ・・・
</ErrorBoundary>
```
　　↓  書き換え
```html
<CustomErrorBoundary>
      ・・・
</CustomErrorBoundary>
```

### ５．１ コンソール出力
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546646/37b458b6-676c-7590-7078-9a78e5d2f8e4.png)
以下が確認できる。
- ```ErrorBoundry```ではなく、```CustomErrorBoundry```でキャッチしている
- ```warn```で出力されていたログが、```LogError```で書き込んでいるので```fail```に変わっている
- ```LogError```で書き込むときに追加したメッセージ「共通エラーハンドリング」が確認できる

### ５．２ Windowsイベントログ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546646/095d2b03-1516-b0d3-c1da-ad162e8429e3.png)

同様の内容が確認できる。





## ６．エラーページの強化
```<ErrorContent>```をコンポーネントとして独立させます

MainLayout.razor
```html
<ErrorContent>
    <p class="text-danger">システムエラーが発生</p>
    <div class="mt-3">
        <a href="" @onclick="NavigateToHome">Homeへ</a>
    </div>
</ErrorContent>
```

　　↓   
```html
<ErrorContent>
    <MyError />
</ErrorContent>
```


/Layout/MyError.razor
```html
<div class="container text-center mt-5">
    <div class="card shadow-lg">
        <div class="card-body">
            <h1 class="text-danger">
                <i class="bi bi-exclamation-triangle-fill"></i> システムエラー
            </h1>
            <p class="text-muted mt-3">
                申し訳ありませんが、問題が発生しました。しばらくしてから再度お試しください。
            </p>
            <div class="mt-4">
                <button class="btn btn-primary btn-lg" @onclick="NavigateToHome">
                    <i class="bi bi-house-door-fill"></i> Homeへ戻る
                </button>
            </div>
        </div>
    </div>
</div>

@code {
    [Inject] private NavigationManager NavigationManager { get; set; } = default!;

    private void NavigateToHome()
    {
        // 完全なページリロードを実行
        NavigationManager.NavigateTo("/", forceLoad: true);
    }
}
```
挙動
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546646/f16d89d2-7368-e710-0350-d269693c96b0.png)

## ７．注意事項
### ７．１ デフォルトの開発環境エラーページ
見慣れたデフォルトのエラーページ（UseDeveloperExceptionPage）ですが、```<ErrorBoundary>```が優先されるため表示されなくなります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546646/0202a0ee-4efb-b951-8206-667002a5d455.png)


### ７．２ デフォルトのエラーページ
デフォルトのprogram.csにUseExceptionHandlerが記述されてます。
```c#
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error", createScopeForErrors: true);
```
こちらも```<ErrorBoundary>```が優先されるため、"/Error"へリダイレクトされません。

## ８．参考
https://learn.microsoft.com/ja-jp/aspnet/core/blazor/fundamentals/handle-errors?view=aspnetcore-9.0

## ９．全ソース
https://github.com/masayahak/BlazorError_InteractiveServer