作者:张富春(ahfuzhang),转载时请注明作者和引用链接,谢谢!


我的某个 thread local 的对象的方法中,抛出了一个异常,且没有捕获。
运行发现程序正常退出,且没有预期中的输出。

如何才能在异常发生时,打印堆栈信息,并且异常退出?
下面是具体做法:

using System;
using System.Threading;
using System.CommandLine;
using System.Threading.Tasks;

internal static class Program {
    private const int UnhandledExceptionExitCode = 99;
    private static int _hasPrintedUnhandledException;
    private static Timer? _unobservedTaskExceptionWatchdog;

    static Program()
    {
        ConfigureGlobalExceptionHandling();
    }

    public static async Task Main(string[] args) {
         // 这里引用 thread local
         // 并且在 thread local 对象中抛出未捕获的异常
    }

    private static void ConfigureGlobalExceptionHandling()
    {
        AppDomain.CurrentDomain.UnhandledException += (_, eventArgs) =>
        {
            PrintUnhandledException("AppDomain.CurrentDomain.UnhandledException", eventArgs.ExceptionObject as Exception);
        };

        TaskScheduler.UnobservedTaskException += (_, eventArgs) =>
        {
            PrintUnhandledException("TaskScheduler.UnobservedTaskException", eventArgs.Exception);
        };

        StartUnobservedTaskExceptionWatchdog();
    }

    private static void StartUnobservedTaskExceptionWatchdog()
    {
        _unobservedTaskExceptionWatchdog = new Timer(_ =>
        {
            try
            {
                GC.Collect();
                GC.WaitForPendingFinalizers();
                GC.Collect();
            }
            catch (Exception ex)
            {
                PrintUnhandledException("UnobservedTaskExceptionWatchdog", ex);
            }
        }, null, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1));
    }

    private static void PrintUnhandledException(string source, Exception? exception)
    {
        if (Interlocked.Exchange(ref _hasPrintedUnhandledException, 1) == 1)
        {
            return;
        }

        Console.Error.WriteLine($"[{DateTimeOffset.UtcNow:u}] Unhandled exception caught from {source}");
        if (exception is null)
        {
            Console.Error.WriteLine("Exception object was null.");
        }
        else
        {
            Console.Error.WriteLine(exception);
        }

        Console.Error.Flush();
        Environment.Exit(UnhandledExceptionExitCode);
    }

}

出现异常后的效果如下:

[2026-02-02 04:47:36Z] Unhandled exception caught from TaskScheduler.UnobservedTaskException
System.AggregateException: A Task's exception(s) were not observed either by Waiting on the Task or accessing its Exception property. As a result, the unobserved exception was rethrown by the finalizer thread. (Connection refused (127.0.0.1:9428))
 ---> System.Net.Http.HttpRequestException: Connection refused (127.0.0.1:9428)
 ---> System.Net.Sockets.SocketException (111): Connection refused
   at System.Net.Sockets.Socket.AwaitableSocketAsyncEventArgs.ThrowException(SocketError error, CancellationToken cancellationToken)
   at System.Net.Sockets.Socket.AwaitableSocketAsyncEventArgs.System.Threading.Tasks.Sources.IValueTaskSource.GetResult(Int16 token)
   at System.Net.Http.HttpConnectionPool.ConnectToTcpHostAsync(String host, Int32 port, HttpRequestMessage initialRequest, Boolean async, CancellationToken cancellationToken)
   --- End of inner exception stack trace ---
   at System.Net.Http.HttpConnectionPool.ConnectToTcpHostAsync(String host, Int32 port, HttpRequestMessage initialRequest, Boolean async, CancellationToken cancellationToken)
   at System.Net.Http.HttpConnectionPool.ConnectAsync(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken)
   at System.Net.Http.HttpConnectionPool.CreateHttp11ConnectionAsync(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken)
   at System.Net.Http.HttpConnectionPool.InjectNewHttp11ConnectionAsync(QueueItem queueItem)
   at System.Threading.Tasks.TaskCompletionSourceWithCancellation`1.WaitWithCancellationAsync(CancellationToken cancellationToken)
   at System.Net.Http.HttpConnectionPool.SendWithVersionDetectionAndRetryAsync(HttpRequestMessage request, Boolean async, Boolean doRequestAuth, CancellationToken cancellationToken)
   at System.Net.Http.RedirectHandler.SendAsync(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken)
   at System.Net.Http.SocketsHttpHandler.<SendAsync>g__CreateHandlerAndSendAsync|115_0(HttpRequestMessage request, CancellationToken cancellationToken)
   at System.Net.Http.HttpClient.<SendAsync>g__Core|83_0(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationTokenSource cts, Boolean disposeCts, CancellationTokenSource pendingRequestsCts, CancellationToken originalCancellationToken)
   at ConsoleLogger.ThreadLocalLogger.writeJsonline(BufferWrapper wrapper) in /home/ahfu/code/github.com/ahfuzhang/QiWa/src/ConsoleLogger/ThreadLocalLogger.cs:line 106
   at ConsoleLogger.ThreadLocalLogger.writeLog(BufferWrapper wrapper) in /home/ahfu/code/github.com/ahfuzhang/QiWa/src/ConsoleLogger/ThreadLocalLogger.cs:line 130
   at ConsoleLogger.ThreadLocalLogger.<>c__DisplayClass16_0.<<TimerLoop>b__0>d.MoveNext() in /home/ahfu/code/github.com/ahfuzhang/QiWa/src/ConsoleLogger/ThreadLocalLogger.cs:line 173
   --- End of inner exception stack trace ---
make: *** [Makefile:20: run] Error 99