1.この記事の目的
Blazorでは<ErrorBoundary>
で効率よくGlobalなエラーハンドリングが実現できます。
また、簡単なカスタマイズでログ出力やエラーページのレイアウト変更など可能なので整理しました。
2.前提条件
- Visual studio 2022 Version 17.12.3
- .Net 8
- UI:Blazor Web App
- Interactive render mode : Server
- Interactivity location : Global
Interactivity location を Global から Per Pageにすると<ErrorBoundary>
でのグローバルエラーハンドリングが複雑になるためここでは考えません。
3.<ErrorBoundary>
3.1 <ErrorBoundary>
の実装
構文
<ErrorBoundary>
<ChildContent>
{正常時の表示コンテント}
</ChildContent>
<ErrorContent>
{異常キャッチ時の表示コンテント}
</ErrorContent>
</ErrorBoundary>
MainLayout.razorにて@Body
を<ErrorBoundary>
で囲みます。
<article class="content px-4">
@Body
</article>
↓ 変更
<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>
3.2 エラー発生タイミングごとの挙動
InteractiveServerレンダーモードではエラー発生タイミングが大きく2種類存在します。
タイミング | 概要 | |
---|---|---|
A | HTTPリクエストの応答中 | GETなどのページ取得時等 |
B | SignalR通信中 | ボタンクリックイベントなど初期ページ表示後のイベント等 |
(A) HTTPリクエストの応答中のエラー
HTTPリクエスト応答中にOnInitializedでエラーを発生させます。
Weather.razor
@page "/weather"
<PageTitle>Weather</PageTitle>
<h1>Weather</h1>
<p>HTTPリクエスト時に強制エラー</p>
@code {
protected override void OnInitialized()
{
throw new Exception("OnInitializedで強制エラー発生");
}
}
エラーがハンドリングされ、<ErrorContent>
で用意したコンテントが表示されます。
(B) SignalR通信中のエラー
Counter.razor
@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");
}
}
}
同じです。
エラーがハンドリングされ、<ErrorContent>
で用意したコンテントが表示されます。
4.デフォルトのログ出力
4.1 コンソール出力
- ErrorBoundryがExceptionをキャッチしたこと
- ThrowしたExceptionのメッセージ「OnInitializedで強制エラー発生」
- エラー発生時のスタックトレース
4.2 Windowsイベントログ
5.ログ出力の強化
<ErrorBoundary>
を継承し、カスタマイズします
OnErrorAsync
をoverride
し、エラー発生時に好みの処理を記述します
/Layout/CustomErrorBoundary.razor
@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
<ErrorBoundary>
・・・
</ErrorBoundary>
↓ 書き換え
<CustomErrorBoundary>
・・・
</CustomErrorBoundary>
5.1 コンソール出力
-
ErrorBoundry
ではなく、CustomErrorBoundry
でキャッチしている -
warn
で出力されていたログが、LogError
で書き込んでいるのでfail
に変わっている -
LogError
で書き込むときに追加したメッセージ「共通エラーハンドリング」が確認できる
5.2 Windowsイベントログ
同様の内容が確認できる。
6.エラーページの強化
<ErrorContent>
をコンポーネントとして独立させます
MainLayout.razor
<ErrorContent>
<p class="text-danger">システムエラーが発生</p>
<div class="mt-3">
<a href="" @onclick="NavigateToHome">Homeへ</a>
</div>
</ErrorContent>
↓
<ErrorContent>
<MyError />
</ErrorContent>
/Layout/MyError.razor
<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);
}
}
7.注意事項
7.1 デフォルトの開発環境エラーページ
見慣れたデフォルトのエラーページ(UseDeveloperExceptionPage)ですが、<ErrorBoundary>
が優先されるため表示されなくなります。
7.2 デフォルトのエラーページ
デフォルトのprogram.csにUseExceptionHandlerが記述されてます。
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error", createScopeForErrors: true);
こちらも<ErrorBoundary>
が優先されるため、"/Error"へリダイレクトされません。
8.参考
9.全ソース