0
votes

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?

2
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
@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
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
@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
@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

2 Answers

0
votes

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.

0
votes

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.