見出し画像

BlazorのErrorBoundaryを使ってみた!!

エラーのハンドリングってめんどくさいですよねぇ。
いろんなエラーのパターンを考えて考えて考えてエラーを処理する。
それでも、いくら頑張って考えたってエラーは起きるものです。
はぁーめんどくさいめんどくさい😩😩😩

でも一番大切なことなんですよね。
私は次の理由からエラーハンドリングを重要視しています!

・ユーザを困らせないためにエラーの状態からアプリを復帰させる
・エラーが発生した状態でユーザにアプリを操作されて
 データが壊れることを防ぐ
・(開発者向けに)エラーの発生内容を分かりやすくする

でも、めんどくさいなぁ😩😩😩


どうも、あっきーです🍉

前置きが長くなりましたが、
めんどくさいエラーハンドリングを簡単に行いたいということで、
今回は.NET6.0で追加されたErrorBoundaryを使ってみたいと思います。


1. ErrorBoundary とは

公式ガイドでは以下のように記載されています。
Boundaryの和訳が"境界"です。

エラー境界

公式ガイドはこちらです。

クラス定義はこちらです。


ざっくりいうと
ErrorBoundary内の子コンテンツでハンドリングできていないエラーを
ErrorBoudaryで拾って制御することができます。
逆に言えば、共通的なエラーは各処理でハンドリングしないで、
ErrorBoundary側で統一してハンドリングすることができます。

次の2点が導入するメリットだと思います。

① 想定外のエラーをErrorBoundaryで拾って制御できる
② ErrorBoundaryでまとめてエラー制御できる

では、導入しなかった場合はどうなるか!?


2. ErrorBoundaryのいないアプリ

想定外のエラーを制御できない

ハンドルしていないエラーが発生した場合、
画面下部に「An unhandled error has occurred. Reload」が表示されてしまいます。

An unhandled error has occurred. Reload

この表示が出たまま操作するとアプリの挙動がおかしくなりますし、
ユーザビリティは悪いですね。
使っていてこの表示されるとすごく不安に感じてしまいます🐸


各処理でエラーハンドリング処理を実装しなければならない

各処理でエラーハンドルするにはtry-catchしてcatchの中を実装する必要があります。

例)"IncrementCount"というクリックイベントのエラー発生時は、
"/error"画面へ遷移させる

@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;
    [Inject]
    protected NavigationManager NavigationManager { get; set; }

    private void IncrementCount()
    {
        try
        {
            currentCount++;
            // 想定外のエラー発生
            throw new Exception();
        }
        catch
        {
            // エラー発生時はerror画面へ遷移させる
            NavigationManager.NavigateTo("/error");
        }
    }
}

これをイベントの数分を実装するのはしんどいですよね。
いくつかは実装漏れもでてくるのではないでしょうか。
いやぁ、しんどい!!!!!😩😩😩😩😩


3. ErrorBoundaryのいるアプリ

いざ!実装!!!!!

想定外のエラーを制御してみる

先ほどのクリックイベントにエラーを混入させる。
※ try-catchを外して想定外のエラーとみなします。

@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;
    [Inject]
    protected NavigationManager NavigationManager { get; set; }

    private void IncrementCount()
    {
        currentCount++;
        // 想定外のエラーを発生させる
        throw new Exception();
    }
}

各ページが表示されるおおもとのコンテンツをErrorBoundaryで囲む。
MainLayout.razorの@bodyを囲みました。

@inherits LayoutComponentBase
<div class="page">
    <div class="sidebar">
        <NavMenu />
    </div>

    <main>
        <div class="top-row px-4">
            <a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a>
        </div>

        <article class="content px-4">
            @* ErrorBoundaryで囲む *@
            <ErrorBoundary>
                @Body
            </ErrorBoundary>
        </article>
    </main>
</div>

さて、動かしてみましょう🐸

MainLayoutにErrorBoundaryを追加

画面下部に表示されていた
「An unhandled error has occurred. Reload」がなくなりましたね!
まだデフォルトのメッセージがそのまま表示されていますので、
メッセージを変えてみましょう!

ErrorContentを追加して、表示したい内容に変更します。

@inherits LayoutComponentBase
<div class="page">
    <div class="sidebar">
        <NavMenu />
    </div>

    <main>
        <div class="top-row px-4">
            <a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a>
        </div>

        <article class="content px-4">
            @* ErrorBoundaryで囲む *@
            <ErrorBoundary>
                <ChildContent>
                    @Body
                </ChildContent>
                @* エラー時に表示したい内容に変える *@
                <ErrorContent>
                    <p class="errorUI">エラーが発生しました🐸リロードしてください🐸</p>
                </ErrorContent>
            </ErrorBoundary>
        </article>
    </main>
</div>

いざ!実行!!🚀

ErrorContentを追加

これで任意の表示ができるようになりましたね🐸

あれ、、、どうやってエラーから復帰するの???

安心してください。履いていますよ🩲
復帰できますよ。

ErrorBoundaryクラスに用意されているRecoverメソッドで
復帰することができます。
リロードボタンをクリックでエラーをリセットするようにしました。

@inherits LayoutComponentBase
<div class="page">
    <div class="sidebar">
        <NavMenu />
    </div>

    <main>
        <div class="top-row px-4">
            <a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a>
        </div>

        <article class="content px-4">
            @* ErrorBoundaryで囲む *@
            <ErrorBoundary @ref="errorBoundary">
                <ChildContent>
                    @Body
                </ChildContent>
                @* エラー時に表示したい内容に変える *@
                <ErrorContent>
                    <p class="errorUI">エラーが発生しました🐸リロードしてください🐸</p>
                    <button @onclick="OnClick">リロード</button>
                </ErrorContent>
            </ErrorBoundary>
        </article>
    </main>
</div>

@code {
    private ErrorBoundary? errorBoundary;

    private void OnClick()
    {
        // エラーのない状態にリセットする
        errorBoundary?.Recover();
    }
}

いざ!実行!!🚀

Recoverメソッドでリセット

リセットされてCounter画面へ戻れましたね。
実はここまでは公式ガイドに記載されています。
※ 若干ソースは変えています。

ここで気を付けないといけない点ですが、
あくまでエラーをリセットして復帰なんですよね。
エラー直前の状態に戻すわけではないのです🙅🙅‍♂️🙅‍♀️

どういうことかというと、、、。
カウントが3の時にエラーになるようにしましょう!

@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;
    [Inject]
    protected NavigationManager NavigationManager { get; set; }

    private void IncrementCount()
    {
        currentCount++;
        if (currentCount == 3)
        {
            // 想定外のエラーを発生させる
            throw new Exception();
        }
    }
}

そうすると、、、、
エラーから復帰するとCurrent countは0の状態になります。
Current countは2に戻りません。

エラーから復帰してもカウントは0

まぁ、よくよく考えるとエラー直前の状態を
保持しているわけではないので、
0に戻りますよね。

基本的には問題ないと思いますが、
次のように動作するアプリは問題がありそうです。

「画面Aで入力→画面Bで入力→画面Cで確認→画面Dで送信」と
手続きを進むアプリがあったとします。
画面Bでエラーが発生‼‼
そのままエラーをリセットして画面Bへ復帰。

アプリの作りにもよると思いますが、
画面Aの状態を保持せずリセットされているはずなので、
データの整合性がなくなるのでは?と。

なので、アプリに合わせてエラーをハンドルしてください!

ここまでで「ErrorBoundary想定外のエラーをキャッチしてエラー制御ができること」が分かりましたね🐸

では、次にいきましょう♪


ErrorBoundaryでまとめてエラーハンドリングを実装する

各イベント処理でcatchの中身を実装するのめんどくさいですよね。
共通したエラーハンドリングはErrorBoundary側に寄せてみましょう。
⚠️ もちろん個別で必要な例外処理は各処理で実装する必要があります。

以下の要件で共通のエラーハンドリングを実装してみましょう!🚀

エラーが発生したときにエラー画面へ遷移する。

まずはエラー画面を作成しましょう!
特にこだわりなくこんな感じでいいでしょう。

@page "/error"

<PageTitle>Error</PageTitle>

<div>ご迷惑をお掛けし申し訳ございません。情シスまでお問い合わせください。</div>
<button @onclick="OnClick">ホームへ戻る</button>

@code {
    [Inject]
    protected NavigationManager NavigationManager { get; set; }
    private void OnClick()
    {
        // ホーム画面へ遷移する
        NavigationManager.NavigateTo("/");
    }
}

次にErrorBoudanryをカスタムしましょう!
MainErrorboundary.razorを作成

@inherits ErrorBoundary

@if (CurrentException == null)
{
    @ChildContent
}
else if (ErrorContent != null)
{
    @ErrorContent(CurrentException)
}
else
{
    <div class=blazor-error-boundary></div>
}

MainErrorboundary.razor.csを作成

using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;

namespace BlazorApp_Test.Organisms
{
    public partial class MainErrorBoundary : ErrorBoundary
    {
        [Inject]
        protected NavigationManager NavigationManager { get; set; }

        protected override Task OnErrorAsync(Exception exception)
        {
            Console.WriteLine($"Error: {exception.Message}");
            NavigationManager.NavigateTo("/error");
            return base.OnErrorAsync(exception);
        }

        protected override void OnParametersSet()
        {
            Recover();
        }
    }
}

次にエラーを仕込みましょう!

こんな感じでエラーを仕込みましょう。

@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>

<button class="btn btn-primary" @onclick="TriggerError">これもエラー</button>

@code {
    private int currentCount = 0;

    private void IncrementCount()
    {
        currentCount++;
        if (currentCount == 3)
        {
            // 想定外のエラーを発生させる
            throw new Exception();
        }
    }

    private void TriggerError()
    {
        // 想定外のエラーを発生させる
        throw new FormatException();
    }
}

それでは、動かしてみましょう🚀

MainErrorBoundary

2つのイベント処理のそれぞれtry-catchはしていませんが、
ちゃんとエラーハンドリングができています。

これで「ErrorBoundaryまとめてエラーハンドリングができること」が分かりましたね🐸


まとめ

最初にあげていたErrorBoundary導入のメリットは伝わったのではないでしょうか?

① 想定外のエラーをErrorBoundaryで拾って制御できる
② ErrorBoundaryでまとめてエラー制御できる

ErrorBoundaryを導入して、
みなさまも快適なエラーハンドリング生活を送ってください!!

最後に

弊社は一緒に働いていただける方を募集中です!
就職/転職活動中や、まだ情報収集中の方、
少しでも興味を持っていただけた方は、以下のアドレスに「note見た!」とご連絡いただけると幸いです💡
プロダクト推進部/採用担当アドレス:pdo_js@persol.co.jp


この記事が気に入ったらサポートをしてみませんか?