顯示具有 Exception 標籤的文章。 顯示所有文章
顯示具有 Exception 標籤的文章。 顯示所有文章
2021-10-16 23:09

用 Exception 作為使用者錯誤訊息的通道

自訂一個 UserException 來傳遞給使用者的訊息,然後程式裡只要簡單的 throw 訊息,接著就是在全域的錯誤處理中將訊息呈現給使用者。

這是一種非常方便的方式,因為可以無視 method 的階層深度,在程式的任何地方都可以將訊息傳到使用者面前。

原則上盡可能將錯誤轉化成 UserException,並且錯誤訊息要能讓使用者[明確知道]要如何正確操作軟體,但是只有工程師才可以修正的錯誤就應該記錄下來,並給使用者這樣的訊息[系統出現錯誤,請通知管理者],只有盡可能的修正錯誤才不會讓使用者常常打電話吵你。

2021-10-15 17:26

全域的 Exception 處理

大部分具有 Exception 機制的程式語言都有提供全域的 Exception 處理,如果你已經有用 log 去記錄錯誤的習慣的話,與其在程式裡佈滿了 try catch,不如在全域處裡中去記錄沒有被 catch 的 Exception,這樣程式就可以更乾淨了,而且程式如果發生異常的關閉時也會進入全域處裡,可以確保 Exception 妥善地被記錄。


Console, WinForm 程式

using System;
using System.Security.Permissions;
using System.Threading;
using System.Windows.Forms;
using NLog;

namespace AppThreadException
{
    static class Program
    {
        private static readonly ILogger _log = LogManager.GetCurrentClassLogger();


        [STAThread]
        [SecurityPermission(SecurityAction.Demand, Flags = SecurityPermissionFlag.ControlAppDomain)]
        public static void Main(string[] args)
        {
            /* ThreadException 用來攔截 UI 錯誤 */
            Application.ThreadException += threadExceptionHandler;

            /* UnhandledException 只能攔截錯誤,不能阻止程式關閉 */
            Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException);
            AppDomain.CurrentDomain.UnhandledException += unhandledExceptionHandler;

            Application.Run(new MyForm());
        }
        

        /// <summary>攔截 UI 錯誤</summary>
        private static void threadExceptionHandler(object sender, ThreadExceptionEventArgs e)
        {
            _log.Fatal(e.Exception, "操作錯誤");
            MessageBox.Show(e.Exception.Message, "操作錯誤", MessageBoxButtons.OK, MessageBoxIcon.Stop);
        }
        

        /// <summary>攔截不可挽回的錯誤,不能阻止程式關閉</summary>
        private static void unhandledExceptionHandler(object sender, UnhandledExceptionEventArgs e)
        {
            Exception ex = (Exception)e.ExceptionObject;

            _log.Fatal(ex, "執行錯誤");
            MessageBox.Show(ex.Message, "執行錯誤", MessageBoxButtons.OK, MessageBoxIcon.Stop);
        }
    }
}

Web 程式

在 Global.asax.cs 中可以設定 Application_Error 就可以攔截未處裡的 Exception,除了用這個方法記錄錯誤,還可以用現有的套件(elmah)幫我們完成。

using System;
using System.Web.Mvc;
using System.Web.Optimization;
using System.Web.Routing;
using NLog;

namespace MvcReport
{
    public class MvcApplication : System.Web.HttpApplication
    {
        private static readonly ILogger _log = LogManager.GetCurrentClassLogger();

        //...

        protected void Application_Error(object sender, EventArgs e)
        {
            Exception ex = Server.GetLastError();
            _log.Fatal(ex, ex.Message);
        }

        //...
    }
}

Net core 程式

Net core 的程式都是相同的方式,Web Request 的要另外配置

using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Autofac.Extensions.DependencyInjection;
using EverTop.Api;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using NLog;


namespace MyWebApp
{
    public class Program
    {
        private static readonly ILogger _log = LogManager.GetCurrentClassLogger();


        public static void Main(string[] args)
        {
            /* UnhandledException 只能攔截錯誤,不能阻止程式關閉 */
            AppDomain.CurrentDomain.UnhandledException += unhandledExceptionHandler;

            /* 用來攔截 Task 錯誤 */
            TaskScheduler.UnobservedTaskException += unobservedTaskException;

            var host = Host.CreateDefaultBuilder(args)
                .UseServiceProviderFactory(new AutofacServiceProviderFactory())
                .ConfigureWebHostDefaults(webHostBuilder => webHostBuilder
                    .UseStartup<Startup>()
                )
                .Build();

            host.Run(); /* 啟動網站 */
        }


        private static void unhandledExceptionHandler(object sender, UnhandledExceptionEventArgs e)
        {
            Exception ex = (Exception)e.ExceptionObject;
            _log.Fatal(ex, "執行錯誤");
        }

        private static void unobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e)
        {
            _log.Fatal(e.Exception, "執行錯誤");
            e.SetObserved();
        }
    }
}

Net core 3.1 Web 程式

在 Startup.cs 中配置 middleware 進行 Exception 攔截

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (_env.IsProduction())
    {
        app.UseExceptionHandler("/Error");
    }
    else
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseStatusCodePagesWithReExecute("/Error");


    /* 利用 middleware 進行 Exception 攔截 */
    /* 這裡的順序很重要,不然會被前面 ExceptionHandler 處理掉就拿不到 Exception */
    app.Use(async (context, next) =>
    {
        try
        {
            await next();
        }
        catch (Exception ex)
        {
            _log.Fatal(ex, "執行錯誤");
            throw; /* 把 Exception 再丟出去給別人處理 */
        }
    });
}

週期性 Thread 的處裡方式

將主要邏輯寫在另外 method 裡,這樣可以專注在 Exception 上。

private bool _runFlag = false;

public void Start()
{
    if (_runFlag) { return; }
    _runFlag = true;

    var thread = new Thread(() =>
    {
        while (_runFlag)
        {
            try
            {
                cycleHandler();
            }
            catch (Exception ex)
            {
                _log.Fatal(ex, "執行錯誤");
            }
            Thread.Sleep(1000);
        }
    });

    thread.Start();
}

public void Stop()
{
    _runFlag = false;
}

private void cycleHandler()
{
    // 主要的邏輯程式寫在這裡
}

PHP 錯誤處理

Ref: set_error_handler, set_exception_handler

<?php
function error_handler($errno, $errstr, $errfile, $errline) {
    if(error_reporting() === 0){ return; }
    throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
}
set_error_handler('error_handler', E_ALL^E_NOTICE);


function exception_handler($exception) {
    // 在這記錄 log
    throw $exception;
}
set_exception_handler('exception_handler');


throw new Exception('Uncaught Exception');
//$a = 1 / 0;

2021-10-14 17:21

Exception 是傳遞訊息的通道

void show(string tag)
{
    Console.WriteLine(tag);
}

void methodA()
{
    throw new Exception("error msg");
    show("A");
}

void methodB()
{
    methodA();
    show("B");
}

void methodC()
{
    try
    {
        methodB();
        show("C");
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.Message);
    }
}

這裡為了方便我們理解將程式展開成下面的樣子:

void show(string tag)
{
    Console.WriteLine(tag);
}

void methodC()
{
    try
    {
        methodB();
            void methodB()
            {
                methodA();
                    void methodA()
                    {
                        throw new Exception("error msg");
    //-----------------------------------------------------
                        show("A");
                    }
                show("B");
            }
        show("C");
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.Message);
    }
}

從範例中我們可以看出兩個特性:


* 通透性

從 throw 的那一行開始 Exception 會一直向上傳遞,直到 catch 的區段。


* 脫離性

從 throw 的那一行之後的程式都不會被執行,會有類似 return 的效果,不同的是會對每一層的 method 都進行脫離,包含 try 的區段也會脫離。


methodC 的 catch 會捕獲從 methodA 丟出的 Exception,並且 Exception 中的 StackTrace 會紀錄 methodA, methodB, methodC,而 show() 都不會被執行。

由於大部分的程式邏輯都可以轉化成 else 就離開的程式流程,正好符合 throw 的脫離性,所以我們一開始在撰寫程式邏輯的時候,可以先專注在一般正常的流程邏輯上,之後再去補足檢查邏輯或脫離邏輯,最後在合適的位置進行 try catch 處理,或者在全域的錯誤處理中進行處理。


這裡有一個糟糕的 method,他用了個 Result 物件來裝載回傳結果以及錯誤訊息:

public class Result
{
    public int Value { get; set; }
    public string Error { get; set; }
}

public Result BadMethod(int input)
{
    var result = new Result();

    if (input > 0)
    {
        result.Value = 100 / input;
    }
    else
    {
        result.Error = "input 需要大於零";
    }
    return result;
}

如果用 Exception 可以讓程式簡單很多:

public int GoodMethod(int input)
{
    if (input <= 0)
    {
        throw new Exception("input 需要大於零");
    }

    return 100 / input;
}
2021-10-13 16:45

初學 Exception 的疑問

一開始學習 Exception 一定會有很多疑問,或者根本沒搞清楚就寫了一堆 try catch,讓程式邏輯變得亂七八糟難以閱讀,甚至小小的修改都不知道從何下手,Exception 是一種邏輯的輔助工具,可以讓你事半功倍,也可以變成噩夢。

我也是摸索了很久才對 Exception 有一定的應用認識,不敢說有多專精,畢竟每種語言跟特性都不同,但也是一定程度的應用經驗,我認為最重要的用途有兩個:


* 用 finally 去關閉資源物件

資源物件有 File, Socket,...,這類的物件在邏輯結束後一定要進行關閉,強調一定一定,實在是吃了自己埋的雷了,沒有關閉的話資源會被咬住,像 Serial Port 這種底層資源沒有關閉的話,想要重新連接就只能重開機了,這是讓人無法接受的。

FileStream file1 = null;
try
{
    file1 = File.Open("<file path>", FileMode.OpenOrCreate);
    // Do something
}
finally
{
    if (file1 != null) { file1.Close(); }
}

/*== C# 可以用 using 代替上面的程式 ==*/
using (FileStream file2 = File.Open("<file path>", FileMode.OpenOrCreate))
{
    // Do something
}


* 用 catch 去記錄 Exception

為了日後可以在日後除錯時留下線索,Exception 的記錄是很重要的,任何程式都有可能出現錯誤,不管是資料錯誤還是邏輯錯誤,有除錯的線索肯定會有很大的幫助,無論程式的大小都應該留下錯誤紀錄。

try
{
    // Do something
}
catch (Expression ex)
{
    _log.Error(ex, "An error occurred.");
}

其餘的清況,在不知道為何要用 try catch 時,最好就不要寫 try catch,基本上程式邏輯一旦出錯就只能退出,如果胡亂地進行修正讓邏輯繼續下去,可能會出現很糟糕的局面,而且資料錯誤就應該修正資料,邏輯錯誤就應該修正邏輯。

錯誤就是要讓人發現才有辦法修正,胡亂的將錯誤屏蔽掉只會埋下未來的苦果,也別用這種方式報復前東家,這只會搞到後面接手的倒楣鬼,本是同根生,相煎何太急。