0

I have the following function that downloads a web page:

    static bool myFunction(int nmsTimeout, out string strOutErrDesc)
    {
        //'nmsTimeout' = timeout in ms for connection
        //'strOutErrDesc' = receives error description as string
        bool bRes = false;
        strOutErrDesc = "";

        HttpClient httpClient = null;
        System.Threading.Tasks.Task<string> tsk = null;

        try
        {
            httpClient = new HttpClient();
            tsk = httpClient.GetStringAsync("https://website-to-connet.com");

            if (tsk.Wait(nmsTimeout))
            {
                if (tsk.Status == System.Threading.Tasks.TaskStatus.RanToCompletion)
                {
                    string strRes = tsk.Result;
                    strRes = strRes.Trim();

                    if (!string.IsNullOrWhiteSpace(strRes))
                    {
                        bRes = true;
                    }
                    else
                    {
                        //Empty result
                        strOutErrDesc = "Empty result";
                    }
                }
                else
                {
                    //Bad task completion
                    strOutErrDesc = "Bad completion result: " + tsk.Status.ToString();
                }
            }
            else
            {
                //Timed out
                strOutErrDesc = "Timeout expired: " + nmsTimeout + " ms.";
            }
        }
        catch (Exception ex)
        {
            //Error
            strOutErrDesc = "Exception: " + ex.Message;
            if (tsk != null)
            {
                strOutErrDesc += " -- ";
                int c = 1;
                foreach(var exc in tsk.Exception.InnerExceptions)
                {
                    strOutErrDesc += c.ToString() + ". " + exc.InnerException.Message;
                }
            }

            bRes = false;
        }

        return bRes;
    }

I thought that my try/catch construct was enough to catch all exceptions in it.

Until I found the following exception and the Windows error message that the app crashed:

enter image description here

Unhandled Exception: 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. ---> System.Net.Http.HttpRequestException: Response status code does not indicate success: 503 (Service Unavailable).

--- End of inner exception stack trace ---

at System.Threading.Tasks.TaskExceptionHolder.Finalize()

What is this and how do I catch it?

c00000fd
  • 20,994
  • 29
  • 177
  • 400
  • 1
    Having the finalizer of the Task class throw this exception is not something a lot of SO users have seen before. It was the behavior of .NET 4.0, back when they still thought it important that programmers were aware that they forgot to check for exceptions. Changed in 4.5 because it is so hard to diagnose and deal with. As-is, the Wait timeout is enough to light the fuse, if it does timeout then the bomb goes off at the next garbage collection, a random amount of time after the mishap. I think you'll need to explicitly cancel the task to fix it. But do move to 4.5+ – Hans Passant Sep 21 '19 at 10:19
  • @HansPassant that's exactly why my language of choice is a native language (C or C++), I want to be in control of all those things. As for this code, it's just an odd-ball project I'm using for myself as a "utility" app. I wrote it in .net primarily for speed. I just don't like it crashing like that. I'll try to search for the `await` keyword to see it fixes it. – c00000fd Sep 21 '19 at 19:56
  • Oops, just realized I can't use `await`.... but moving it to `4.5` just to fix this "overthought" GC exception is kinda silly. Any other idea how I can catch it in .net 4.0? The annoying thing is that I can't even repro it. If I give it the wrong URL to open, it's caught in my `try/catch` block. – c00000fd Sep 21 '19 at 23:55
  • @c00000fd You should be able to reproduce it by setting some really short time in the `Wait` function (like 1 ms) while requesting a page that does not return success (as your 503 example). Then it should fail. After that you just need to wait or force garbage collection (`GC.Collect()`) and the finalizer thread should throw the exception. – Ordoshsen Sep 22 '19 at 09:54
  • @Ordoshsen: Thanks for trying to help. I did just like you suggested: I created my own PHP page that simply outputs a 503 status header. Then changed the wait timeout to 1 ms and added `GC.Collect();` after my `tsk.Wait(1)`. In this case the `Wait` function simply returns `false` and `GC.Collect();` does nothing. In other words, no exception. If I make the timeout larger, in this example, `tsk.Wait()` throws the `System.AggregateException` that is caught in my `try/catch` block. The question is how did it crash in my screenshot? (So here's your "safe" code.) – c00000fd Sep 23 '19 at 00:45
  • You need to wait for the task to fail first. You cannot just use `Wait` because it would check for the exception (which however is your goal), but you can wait actively with something like `while (!tsk.IsCompleted);` – Ordoshsen Sep 23 '19 at 08:21
  • However did you try the things I posted in the answer? Did it not work and if so, was there any difference? – Ordoshsen Sep 23 '19 at 08:22

2 Answers2

0

The app crashed, because try/catch is not "catch it everywhere" hack, it only catches exceptions that are thrown on the same call stack, or same call context.

You, on the other hand, for some reason use synchronous method to launch async tasks, those are run on other threads, and the context for them is lost.

Either use sync versions of those methods, or better else, use async method and await on your async tasks, that will preserve the call context and will allow you to catch any exception thrown from within with your try/catch block.

AgentFire
  • 8,944
  • 8
  • 43
  • 90
  • Sorry, forgot to add `.NET 4.0` tag. I don't code in .net much. So await` is not available for me. As for synchronous methods, then `HttpClient` doesn't seem to have them. So having said that, how do I ensure that all exceptions in the async thread are caught by my function? – c00000fd Sep 21 '19 at 23:51
  • @c00000fd Can [this topic](https://stackoverflow.com/questions/19423251/async-await-keywords-not-available-in-net-4-0) help you with that? – AgentFire Sep 22 '19 at 12:16
0

Note that what is described here is an old behaviour (as stated in the comments) and it does not happen with .NET 4.5 and newer.

What is happening is that the Task did not finish successfully and you are not checking for errors. When the garbage collector tries to clean up the Task object, it finds the unhandled exception there and throws it in an AggregateException. That is the exception is not actually thrown in your try block (it's even on different thread), hence your catch cannot catch it.

What you want to do is to properly await the created task. You might want to read up on async/await in C# at this point. If you want the task to be cancellable, you may have to use GetAsync with a cancellation token or you will have to wait for GetStringAsync to finish at some point.

If you for some reason do not want to use the asynchronous way of awaiting (you should!), you can still use tsk.Wait();. This will however wrap the thrown exception in an AggregateException and the call will be synchronous.

And if you really cannot stay around and wait for your function to finish, you can see in this question, how to handle the exception checking automatically with a continuation task.

However I would really advise you to use async/await and properlly check how the tasks finish and what did they throw.

Ordoshsen
  • 650
  • 3
  • 15
  • Like I said in another comment, I forgot to mention that it turns out that I'm coding it in `.net 4.0`. (I'm rarely using .net.) So having said that, I can't use `await`. So any ideas how I can catch those AggregateExceptions in a worker thread? – c00000fd Sep 21 '19 at 23:52
  • @c00000fd I have joined the .NET train fairly recently, so I am not 100% sure when the features came out, but I believe continuation tasks should be available from the beginning of tasks, so you can use the hack in the linked question. Or you can just synchronously wait for task to finish with `Wait` or `Result` and that should throw the exception. Or you can save the task, do some other stuff and when it finishes check the `Exception` property. – Ordoshsen Sep 22 '19 at 09:47
  • Bottom line is whether you just want to ignore/log the exception, or you want to catch it in this specific part of code, or want to continue as fast as possible and maybe handle it later – Ordoshsen Sep 22 '19 at 09:48