Response.Redirect and the ThreadAbortException, and why it’s a good thing

Posted April 5, 2008 in asp.net
Reading time: 5 minutes

A couple of months ago, I ran into a problem where I was seeing a bunch of ThreadAbortExceptions showing up in my logs. It didn’t take long to track down why – I was calling Response.Redirect from within a try/catch block in my ASP.NET code-behind page, and the catch block was catching the ThreadAbortException and writing it out to the log. But why was the ThreadAbortException being generated?

It turns out that Response.Redirect, when called with just the URL, or with the URL and a Boolean value of true, calls the Response.End method. Response.End then calls Thread.Abort on the current thread, and, assuming that the page is in a cancellable state, this method throws the ThreadAbortException, alerting the framework that it is time to stop this thread, and NOW.

Why is this a good thing?

Let’s step back a bit and look at the recommended workaround for dealing with the ThreadAbortException. Googling for an answer yields many suggestions to call Response.Redirect and pass the Boolean value false as the second, optional parameter. This effectively suppresses the ThreadAbortException, preventing it from being caught and potentially logged as an error.

All is well and good, right? Isn’t this what we want?

Not quite.

One side-effect of suppressing the ThreadAbortException is that any code after Response.Redirect is still executed. This includes any event handlers in the page lifecycle that have yet to execute. This is a waste of system resources, particularly if any of these event handlers contains resource-intensive code.

For the sake of illustration, let’s assume that our page, Default.aspx, has event handlers for most of the Page’s events:

  • Page_PreInit
  • Page_Init
  • Page_InitComplete
  • Page_PreLoad
  • Page_Load
  • Page_LoadComplete
  • Page_PreRender
  • Page_PreRenderComplete
  • Page_SaveStateComplete

Let’s further assume that each of these event handlers contains a resource- and time-intensive operation. It could be a database call, a network call, or a batch image manipulation. Whatever it is, it requires a lot of resources and a lot of time.

Each event handler will append a string to a StringBuilder member variable called _EventsFired. Immediately preceding the call to Response.Redirect, _EventsFired will be stored as a Session variable so that the error page, Error.aspx, can display which methods were called. The Redirect will occur in the Page_Load method; the user will be sent from Default.aspx to Error.aspx, which will then display the contents of _EventsFired.

Here is the code listing of Default.aspx’s Page_Load event handler:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
protected void Page_Load (Object sender, EventArgs e)
{
    _EventsFired.Append ("Page_Load called.  Resource-intensive operation executed.<br />");

    try
    {
        Session["ErrorMessage"] = _EventsFired;

        Response.Redirect ("Error.aspx", false);
        Context.ApplicationInstance.CompleteRequest ();
    }
    catch (Exception ex)
    {
        _EventsFired.Append (ex.ToString ());
    }
}

Notice how we are passing false as the second parameter of Response.Redirect, meaning that we will prevent a ThreadAbortException from being thrown.

Here is the output from Error.aspx:

Page_PreInit called. Resource-intensive operation executed.
Page_Init called. Resource-intensive operation executed.
Page_InitComplete called. Resource-intensive operation executed.
Page_PreLoad called. Resource-intensive operation executed.
Page_Load called. Resource-intensive operation executed.
Page_LoadComplete called. Resource-intensive operation executed.
Page_PreRender called. Resource-intensive operation executed.
Page_PreRenderComplete called. Resource-intensive operation executed.
Page_SaveStateComplete called. Resource-intensive operation executed.
Render called.

Page execution did not stop at Page_Load, even though we told the Application to complete the request! In all likelihood, this is not the desired behavior, especially if any of the post-Load event handlers contains time-consuming or resource-intensive operations.

Now let’s modify Default.aspx’s Page_Load event handler to call Response.Redirect without suppressing a ThreadAbortException from being thrown:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
protected void Page_Load (Object sender, EventArgs e)
{
    _EventsFired.Append ("Page_Load called.  Resource-intensive operation executed.<br />");

    try
    {
        Session["ErrorMessage"] = _EventsFired;

        Response.Redirect ("Error.aspx");
    }
    catch (Exception ex)
    {
        _EventsFired.Append (ex.ToString ());
    }
}

Here is the output from Error.aspx (remember, this is the list of events that fired during the processing of Default.aspx):

1
2
3
4
5
6
Page_PreInit called. Resource-intensive operation executed.
Page_Init called. Resource-intensive operation executed.
Page_InitComplete called. Resource-intensive operation executed.
Page_PreLoad called. Resource-intensive operation executed.
Page_Load called. Resource-intensive operation executed.
System.Threading.ThreadAbortException: [snip] Default.aspx.cs:line 45

It’s almost what we want, but notice that we “logged” the ThreadAbortException. We need a way to exclude that from being logged.

Let’s modify Default.aspx’s Page_Load event handler one more time:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
protected void Page_Load (Object sender, EventArgs e)
{
    _EventsFired.Append ("Page_Load called.  Resource-intensive operation executed.<br />");

    try
    {
        Session["ErrorMessage"] = _EventsFired;

        Response.Redirect ("Error.aspx");
    }
    catch (System.Threading.ThreadAbortException)
    {
        throw;
    }
    catch (Exception ex)
    {
        _EventsFired.Append (ex.ToString ());
    }
}

Here is the output from Error.aspx:

1
2
3
4
5
Page_PreInit called. Resource-intensive operation executed.
Page_Init called. Resource-intensive operation executed.
Page_InitComplete called. Resource-intensive operation executed.
Page_PreLoad called. Resource-intensive operation executed.
Page_Load called. Resource-intensive operation executed.

Success! Finally, the desired result!

  • The page stopped processing when it was supposed to, right after the call to Response.Redirect.
  • Event handlers later on in the page lifecycle were not unnecessarily executed.
  • The ThreadAbortException was not “logged” by our catch block. Instead, it was explicitly caught and rethrown up the stack.

So there we have it. Suppressing the Response.Redirect-generated ThreadAbortException can be a bad thing, particularly if there is a lot of resource-intensive code left to execute after the redirect. To prevent those pesky ThreadAbortExceptions from showing up in our log files, all we have to do is explicitly catch them and rethrow them. On busy sites, this can help to conserve scarce resources.

Used properly, ThreadAbortException is your friend.

Update 2007-08-01 07:44

Of course, I could have moved the Response.Redirect outside of the try/catch. However, the code that prompted the writing of this article is legacy production code that can’t be modified at my whim, hence the need for this approach.

Update 2008-04-05: Moved over from jonsagara.com.



Comments

comments powered by Disqus