見出し画像

BlazorのErrorBoundaryを活用してみた!!

BlazorのErrorBundaryって便利ですよね。
もうErrorBoundaryのない世界を想像できません🐈
ちなみに読み方は"エラーバウンダリ"です。


どうも、あっきーです🍊

前回、BlazorのErrorBoundaryを紹介させていただきました。

前回に続いて、もう少し活用例を考えていきたいと思います。


1. エラーの内容に合わせてエラーハンドリングする

前回まではエラーハンドリングをErrorBoundaryで実装して
処理を共通化しました。

次は発生したエラーに合わせて
エラーハンドリングの処理を変えてみたいと思います。

まずはイメージしやすいように実装後の動きをご覧ください!

Error Handling

【カウント3でエラー】
 予期せぬエラーの発生を想定しエラー画面へ遷移する。

【クライアントの謎エラー】
 クライアントサイドのエラーが発生を想定し、
 クライアントサイドエラー画面へ遷移する。

【API実行時エラー】
 API実行中のエラーの発生を想定し、
 アラートの表示と再実行を促す。
 画面は自動で復帰させる。

上記のように発生したエラーに合わせて、
エラー後の動きをErrorBoudary側で実装することができました。

次のように実装しました。
※ あくまで簡易的な実装ですのでその点はご了承ください。

まずはエラーの内容を判別できるようにExceptionを定義します🚨
今回は2種類用意しました。
(もちろん標準のExceptionを使ってもいいと思います。)

FrontException.cs
特に今回は特別なことはしていないですが、
ErrorCodeなど保持させて画面に表示させてもいいですよね!

namespace BlazorApp_Test.Components.Exeptions
{
    public class FrontException : Exception
    {
        public FrontException() { }

        public FrontException(string message) : base(message) { }

        public FrontException(string message, Exception innerException) : base(message, innerException) { }
    }
}

ApiException.cs

namespace BlazorApp_Test.Components.Exeptions
{
    public class ApiException : Exception
    {
        public ApiException() { }

        public ApiException(string message) : base(message) { }

        public ApiException(string message, Exception innerException) : base(message, innerException) { }
    }
}

次にエラー画面を作成です💻
こちらも2種類用意しました。

Error.razor
エラーメッセージの表示とホーム画面へ戻れるようにしています。

@page "/error"

<PageTitle>Error</PageTitle>

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

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

FrontError.razor
Error.razorと同じです。

@page "/front-error"

<PageTitle>FrontError</PageTitle>

<div>フロントのエラーが発生⚠️⚠️⚠️</div>
<button @onclick="OnClick">ホームへ戻る</button>

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

やっと主役のErrorBoundaryの実装です🫅

MainErrorBoundary.razor
ApiExceptionの時にアラートを表示させるようにしました。

@using BlazorApp_Test.Components.Exeptions
@inherits ErrorBoundary

@if (CurrentException == null)
{
    @ChildContent
}
else if(CurrentException is ApiException)
{
    // アラートで通知(実装めんどくさいのでjsで対応します)
    <script>
        alert("通信中にエラーが発生しました。再実行してください。");
    </script>
    @ChildContent
}
else if (ErrorContent != null)
{
    @ErrorContent(CurrentException)
}
else
{
    <div class=blazor-error-boundary></div>
}

MainErrorBoundary.razor.cs
Exceptionに合わせて処理を変えています。

using BlazorApp_Test.Components.Exeptions;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;

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

        protected override Task OnErrorAsync(Exception exception)
        {
            // エラーをハンドリングする
            HandleError(exception);
            return base.OnErrorAsync(exception);
        }

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

        private void HandleError(Exception exception)
        {
            if (exception is FrontException)
            {
                NavigationManager?.NavigateTo("front-error");
            }
            else if (exception is ApiException)
            {
                //
            }
            else
            {
                // 想定外のエラー
                NavigationManager?.NavigateTo("error");
            }
        }
    }
}

最後にエラーを発火する画面です。
Counter.razor

@page "/counter"
@using BlazorApp_Test.Components.Exeptions

<PageTitle>Counter</PageTitle>

<h1>Counter🐸</h1>

<p role="status">Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">カウント3でエラー</button>
<button class="btn btn-primary" @onclick="FrontError">クライアントの謎エラー</button>
<button class="btn btn-primary" @onclick="ApiError">API実行時エラー</button>

@code {
    private int currentCount = 0;
    [Inject]
    protected IJSRuntime JSRuntime { get; set; }

    private void IncrementCount()
    {
        currentCount++;
        if (currentCount == 3)
        {
            // 予期せぬエラーを発生させる
            throw new Exception();
        }
    }

    private void FrontError()
    {
        try
        {
            Console.WriteLine("何かの処理を実行!!");
            throw new Exception();
        }
        catch
        {
            throw new FrontException();
        }
    }

    private void ApiError()
    {
        try
        {
            Console.WriteLine("APIを実行!!");
            throw new Exception();
        }
        catch
        {
            throw new ApiException();
        }
    }
}

今回は3パターンに分けて実装してみました。
エラーの内容に合わせてハンドリングができますね。


2. JSのエラーもハンドリングする

今まではすべてBlazorに閉じたエラーのハンドリングでした。

ブラウザで実行されるアプリだと、
どうしてもJSを使うことが出てきますよね。
JSで起きるエラーもハンドリングしておきましょう!

相互運用中のJSエラーをハンドリングする

まずはエラーを発生させてみます。

Counter.razor
JSのtrrigerJsError関数をBrazorから呼び出しを追加

@page "/counter"
@using BlazorApp_Test.Components.Exeptions

<PageTitle>Counter</PageTitle>

<h1>Counter🐸</h1>

<p role="status">Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">カウント3でエラー</button>
<button class="btn btn-primary" @onclick="FrontError">クライアントの謎エラー</button>
<button class="btn btn-primary" @onclick="ApiError">API実行時エラー</button>
<button class="btn btn-primary" @onclick="JsError">JSエラー</button>


@code {
    private int currentCount = 0;
    [Inject]
    protected IJSRuntime? JSRuntime { get; set; }

    // 一部省略

    private async Task JsError()
    {
        Console.WriteLine("JSを実行!!");
        await JSRuntime.InvokeVoidAsync("trrigerJsError");
    }
}

common.js
JSのtrrigerJsError関数を実装。
エラーを発火させる🔥🔥🔥

(() => {
    window.trrigerJsError = () => {
        console.log('trrigerJsError');
        throw new Error("JS Error");
    };
})();

index.html
common.jsを指定して参照できるようにする

<!DOCTYPE html>
<html lang="en">

<head>
    <!-- 省略 -->
</head>

<body>
    <div id="app">
    <!-- 省略 -->
    </div>

    <!-- common.jsを指定 -->
    <script src="js/common.js?v=0.0.0"></script>
    <script src="_framework/blazor.webassembly.js"></script>
</body>

</html>

MainErrorBoundary.cs
JSExceptionの分岐を追加してフロントエラー画面へ遷移させる

using BlazorApp_Test.Components.Exeptions;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.JSInterop;

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

        protected override Task OnErrorAsync(Exception exception)
        {
            // エラーをハンドリングする
            HandleError(exception);
            return base.OnErrorAsync(exception);
        }

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

        private void HandleError(Exception exception)
        {
            if (exception is FrontException)
            {
                NavigationManager?.NavigateTo("front-error");
            }
            else if (exception is ApiException)
            {
                //
            }
            else if (exception is JSException)
            {
                // JSエラー
                NavigationManager?.NavigateTo("front-error");
            }
            else
            {
                // 想定外のエラー
                NavigationManager?.NavigateTo("error");
            }
        }
    }
}

いざ!動確!!🚀

JSException

JSExceptionは.NETからJSへの相互運用呼び出し中に発生するエラーです。
JSExceptionを条件分岐に加えることで
JS相互運用中に発生したエラーのハンドリングも可能になります。

相互運用外のJSエラーもハンドリングする

JSExceptionでは相互運用外のJSエラーはハンドリングできません。
確認してみましょう!!

common.jsを少し変更してみます。
setTimeoutで相互運用完了後にエラーを発生させます。

(() => {
    window.trrigerJsError = () => {
        console.log('trrigerJsError');
        // throw new Error("JS Error");
        setTimeout(() => { throw new Error("JS Error"); }, 0);
    };
})();

いざ!確認!🚀

JS Error

Consoleを見るとJSエラーが吐かれていますが、
ErrorBoundaryではキャッチできていないため、
ハンドリングができていません。
このままだと、
アプリが壊れた状態で操作がされてしまう可能性があります😢

まず、実装前に完成後の姿をお見せします📺

HandleJSError

Consoleを見るとJSエラーが吐かれていて、
かつフロントエラー画面へ遷移しています。
これはErrorBoundaryでエラーハンドリングを行っています。

それでは実装を見て行きましょう👀👀👀
common.jsにJSエラーイベントを監視します🎦
JSエラー発生時のエラー情報を
BrazorのHandleJSErrorメソッドに渡すようにします。

(() => {
    // 一部省略

    window.errorBoundaryInterop = {
        setup: function (dotNetObj) {
            window.addEventListener('error', function (event) {
                const { message: _msg, error, filename } = event;
                const {
                    name,
                    message = '',
                    stack = '',
                } = error ?? {
                    name: '',
                    message: _msg,
                    // error情報を取得できない場合はerror発生ファイルを送付
                    stack: `filename: ${filename}`,
                };
    
                dotNetObj.invokeMethodAsync('HandleJSError', name, message, stack);
            });
        },
    };
})();

MainErrorBoundary.razor.csで
errorBoundaryInterop.setupの呼び出しと
HandleJSErrorメソッドを実装します。

using BlazorApp_Test.Components.Exeptions;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.JSInterop;

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

        protected override Task OnErrorAsync(Exception exception)
        {
            // 省略
        }

        protected override void OnParametersSet()
        {
            // 省略
        }

        protected override async Task OnAfterRenderAsync(bool firstRender)
        {
            if (firstRender)
            {
                await JSRuntime.InvokeVoidAsync("errorBoundaryInterop.setup", DotNetObjectReference.Create(this));
            }

            await base.OnAfterRenderAsync(firstRender);
        }

        private void HandleError(Exception exception)
        {
            // 省略
        }

        [JSInvokable("HandleJSError")]
        public void HandleJSError(string name, string message, string stack)
        {
            HandleError(new FrontException(message, stack));
        }
    }
}

これでJSエラーもErrorBoundaryで
エラーハンドリングできるようになりました。

うん、良きですね!
どんどんエラーハンドリング処理を
一か所にまとめられるようになってきました!


最後に

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


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