7

I have a WinForm application written in C# where I put a try-catch block in the Program.cs, in the program entry, the static void Main method, right in the beginning of the application like this:

using System;
using System.IO;
using System.Windows.Forms;

namespace T5ShortestTime {
    static class Program {
        /// <summary>
        /// The main entry point for the application.
        /// </summary>
        [STAThread]
        static void Main() {
            try {
                Application.EnableVisualStyles();
                Application.SetCompatibleTextRenderingDefault(false);
                Application.Run(new T5ShortestTimeForm());
            } catch (Exception e) {
                string errordir = Path.Combine(Application.StartupPath, "errorlog");
                string errorlog = Path.Combine(errordir, DateTime.Now.ToString("yyyyMMdd_HHmmss_fff") + ".txt");
                if (!Directory.Exists(errordir))
                    Directory.CreateDirectory(errordir);                
                File.WriteAllText(errorlog, e.ToString());              
            }
        }
    }
}

As you can see, the Application is put in a try-catch block and in the catch block, the only thing it does is to create an error log file.

Now, so far so good. My application is running well and if I encounter a crash, the last Exception should be captured by the try-catch block and stored in the error log file.

However, as I run my program for a while, I get an unhandled exception (null reference). What surprise me is that the exception does not create an error log file.

Now, this post shows that it is possibly caused by ThreadException or HandleProcessCorruptedStateExceptions (the two most upvoted answers), but my case shows a simple null reference exception:

Problem signature:
  Problem Event Name:   CLR20r3
  Problem Signature 01: T5ShortestTime.exe
  Problem Signature 02: 2.8.3.1
  Problem Signature 03: 5743e646
  Problem Signature 04: T5ShortestTime
  Problem Signature 05: 2.8.3.1
  Problem Signature 06: 5743e646
  Problem Signature 07: 182
  Problem Signature 08: 1b
  Problem Signature 09: System.NullReferenceException
  OS Version:   6.3.9600.2.0.0.272.7
  Locale ID:    1033
  Additional Information 1: bb91
  Additional Information 2: bb91a371df830534902ec94577ebb4a3
  Additional Information 3: aba1
  Additional Information 4: aba1ed7202d796d19b974eec93d89ec2

Read our privacy statement online:
  http://go.microsoft.com/fwlink/?linkid=280262

If the online privacy statement is not available, please read our privacy statement offline:
  C:\Windows\system32\en-US\erofflps.txt

Why would that be?

Community
  • 1
  • 1
Ian
  • 30,182
  • 19
  • 69
  • 107
  • 2
    That's not how you create a global exception handler. Look to the right of this page, in the 'Linked' section. The accepted answer there tells you what to do. – jmcilhinney May 25 '16 at 05:12
  • @jmcilhinney you mean `ThreadException`? – Ian May 25 '16 at 05:14
  • In the example, there is // Start a new thread, separate from Windows Forms, that will throw an exception. `private void button2_Click(object sender, System.EventArgs e) { ThreadStart newThreadStart = new ThreadStart(newThread_Execute); newThread = new Thread(newThreadStart); newThread.Start(); }` which purposely handle exception in a new thread. But can that creates `null` reference exception (like my case) instead of `ThreadException` (supposed to be this type - is it not?)? – Ian May 25 '16 at 05:17

3 Answers3

5

the last Exception should be captured by the try-catch block

That is not going to happen. Except in one case, when you run your program with a debugger attached. So you surely got lulled into believing it would work, everybody always starts out running their program with F5 for a while.

Application.Run() has a back-stop in its code that raises events, try/catch-em-all that raises the Application.ThreadException event when an event handler throws an unhandled exception. That back-stop is really, really necessary, especially on the x64 version of Windows 7. Very Bad Things happen when there is no exception handler. That back-stop is however not in place when you run with the debugger, that makes unhandled exceptions too difficult to debug.

So when you debug then your catch clause will run. Making unhandled exceptions too difficult to debug. When you run without a debugger then your catch clause will not run and your program will crash, just as you described. Making unhandled exception too difficult to debug.

So don't do it this way. How Application.Run() deals with unhandled exceptions is configured with the Application.SetUnhandledExceptionMode() method. You'll like this version better:

    [STAThread]
    static void Main() {
        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);
        if (!System.Diagnostics.Debugger.IsAttached) {
            Application.SetUnhandledExceptionMode(UnhandledExceptionMode.ThrowException);
            AppDomain.CurrentDomain.UnhandledException += LogException;
        }
        Application.Run(new Form1());
    }

    private static void LogException(object sender, UnhandledExceptionEventArgs e) {
        string errordir = Path.Combine(Application.StartupPath, "errorlog");
        string errorlog = Path.Combine(errordir, DateTime.Now.ToString("yyyyMMdd_HHmmss_fff") + ".txt");
        if (!Directory.Exists(errordir))
            Directory.CreateDirectory(errordir);
        File.WriteAllText(errorlog, e.ToString());
        AppDomain.CurrentDomain.UnhandledException -= LogException;
        MessageBox.Show("Error details recorded in " + errorlog, "Unexpected error");
        Environment.Exit(1);
    }

With this code in place, you can debug unhandled exceptions without any problems. The Debugger.IsAttached test ensures that the debugger will always stop when an event handler falls over. Without a debugger, it then disables the Application.ThreadException event (it is quite useless) and favors listening to all exceptions. Including the ones raised in worker threads.

You ought to give an alert to the user so the window doesn't just disappear without any trace. I was going to recommend MessageBox but noticed that this bug is currently back again on Windows 10. Sigh.

Community
  • 1
  • 1
Hans Passant
  • 922,412
  • 146
  • 1,693
  • 2,536
  • Thanks for the explanation, this should be the answer. One last question, you are saying that it is especially bad for Windows 7 x64, I use Windows 10 x64. Does that make any difference? In your post, you stated, "Update to Windows 8 or later, they have this wow64 problem solved." - I assume that this apply to windows 10 too, is this correct? – Ian May 25 '16 at 06:42
  • Win10 does not have that problem, it is highly specific to Win7. – Hans Passant May 25 '16 at 06:43
  • Ok, thanks. Then I will have peace in mind... ;) I also notice that the `Dispose` is still called *after* the `Exception`. I implement the `SetUnhandledExceptionMode` to `ThrowException`, I tested it with a `throw new System.Exception()`. Surprisingly, the program generates *two* error log files instead of *one*. One is the `System.Exception`, and the other is the `Exception` in the `Dispose` - and the one in the `Dispose` is produced *later*. Why is this so? Is this actually expected that the `Dispose` is still called *after* a fatal Exception? – Ian May 25 '16 at 06:49
  • 1
    It is not very clear what Dispose method you are talking about. Presumably it is actually the Dispose(bool) overload, it is also called by the finalizer. You get to choose how you terminate your program, Environment.Exit() still cleans up by running finalizers, Environment.FailFast() does not. Dispose(bool) should not do anything dangerous when the *disposing* argument is false, you never want to crash the finalizer thread. If that doesn't help then click the Ask Question button. – Hans Passant May 25 '16 at 06:54
  • The explanation in the comment is helpful. It seems like the second `Exception` is caused by the `Environment.Exit`. When I did not put the `Environment.Exit` but replace with `Environment.FailFast` (and yes, it is the default *parameterless* `Dispose` had by the `WinForm`), the program did *not* produce two logs. I would also expect that it would do the same if `AppDomain.CurrentDomain.UnhandledException -= LogException;` line is used. Thanks again! ;) – Ian May 25 '16 at 07:05
2

ThreadException is not an exception type like (NullReferenceException). It is that:

This event allows your Windows Forms application to handle otherwise unhandled exceptions that occur in Windows Forms threads

This means that it handles exceptions in threads other than the Main Thread.

So, you need to subscribe to : AppDomain.CurrentDomain.UnhandledException also in order to handle the exceptions in your Main Thread (Regardless of the type of the exception e.g. NullReference, IndexOutOfRange, etc..).

Zein Makki
  • 29,485
  • 6
  • 52
  • 63
2

Ok, in the end I implement Application.SetUnhandledExceptionMode(UnhandledExceptionMode.ThrowException) as exampled by Hans Passant for VB.Net in this post. Here I put my own code + error logging for C#:

static void Main() {
    Application.EnableVisualStyles();
    Application.SetCompatibleTextRenderingDefault(false);
    if (!System.Diagnostics.Debugger.IsAttached) {
        Application.SetUnhandledExceptionMode(UnhandledExceptionMode.ThrowException);
        AppDomain.CurrentDomain.UnhandledException += LogUnhandledExceptions;
    }
    Application.Run(new T5ShortestTimeForm());
}

private static void LogUnhandledExceptions(object sender, UnhandledExceptionEventArgs e) {
    Exception ex = (Exception)e.ExceptionObject;
    string errordir = Path.Combine(Application.StartupPath, "errorlog");
    string errorlog = Path.Combine(errordir, DateTime.Now.ToString("yyyyMMdd_HHmmss_fff") + ".txt");
    if (!Directory.Exists(errordir))
        Directory.CreateDirectory(errordir);
    File.WriteAllText(errorlog, ex.ToString());
    Environment.Exit(System.Runtime.InteropServices.Marshal.GetHRForException(ex));
}

Also, it seems like the source of confusion here is that there are actually two propagated Exceptions occur:

The first one was any Exception from the application itself:

System.Exception: Exception of type 'System.Exception' was thrown.
   at T5ShortestTime.T5ShortestTimeForm..ctor() in C:\Test.cs:line 45
   at T5ShortestTime.Program.Main() in C:\Test.cs:line 19
   at ...

And the second one occurs during the Dispose of the Form components, which creates another exception, and it is the null reference exception:

System.NullReferenceException: Object reference not set to an instance of an object.
   at T5ShortestTime.T5ShortestTimeForm.Dispose(Boolean disposing)
   at System.ComponentModel.Component.Finalize()

So, when I test the exception in my app, the NullReferenceException comes the last, in the Dispose.

enter image description here

I only manage to capture this after I set the UnhandledExceptionMode to ThrowException above.

Community
  • 1
  • 1
Ian
  • 30,182
  • 19
  • 69
  • 107