Click here to Skip to main content
Click here to Skip to main content

Back to the basics: Exploration of approaches to handle ThreadAbortException with Response.Redirect()

, 30 May 2011
Rate this:
Please Sign up or sign in to vote.
Handling ThreadAbortException is easy, if you know what is happening inside.

Introduction

The story began very simply.

One day, while working, I got an error mail from one of our ASP.NET applications deployed in the production site. The error mail was something like the following:

Timestamp: 7/2/2010 7:26:15 PM
Message: HandlingInstanceID: 71393a92-8cd9-42f5-9572-ca172ae0ca03
An exception of type 'System.Threading.ThreadAbortException' occurred 
and was caught.

Type : System.Threading.ThreadAbortException, mscorlib, 
Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 
Message : Thread was being aborted.
Source : mscorlib
Help link :
ExceptionState : System.Web.HttpApplication+CancelModuleException
Data : System.Collections.ListDictionaryInternal
TargetSite : Void AbortInternal()
Stack Trace : at System.Threading.Thread.AbortInternal()
at System.Threading.Thread.Abort(Object stateInfo)
at System.Web.HttpResponse.End()
at System.Web.HttpResponse.Redirect(String url, Boolean endResponse)
at System.Web.HttpResponse.Redirect(String url)
at MyDashboard.Page_Load(Object sender, EventArgs e)

Guess what, we have a simple exception reporting system at each of our ASP.NET production sites. Whenever an unhandled exception occurs there, the exception information is captured and emailed to a pre-configured email address so that we can investigate and fix it.

To do this, we usually put a few lines of codes in the Global.asax, which catches any unhandled exception and emails the exception details to us. Here is the code:

void Application_Error(object sender, EventArgs e)
{ 
    // Code that runs when an un-handled error occurs
    Exception ex = Server.GetLastError();
    Logger.LogException(ex);
}

So the above exception information was being emailed by this tiny little friendly code, which saves our life often.

Why this exception occurred?

There must be some unhandled exception occurring behind this error report. While investigating, I noticed that a Response.Redirect() statement was behind this exception. See the callstack information from top to bottom:

at System.Threading.Thread.Abort(Object stateInfo)
at System.Web.HttpResponse.End()
at System.Web.HttpResponse.Redirect(String url, Boolean endResponse)
at System.Web.HttpResponse.Redirect(String url)
at MyDashboard.Page_Load(Object sender, EventArgs e)

Analyzing MyDashboard.aspx.cs, I found that there is indeed a Response.Redirect() statement, which looked pretty harmless to me:

if(!UserHasPermissionToViewThisPage())
{
    Response.Redirect("Unauthorized.aspx");
}

Hm.. interesting. Looked like the Response.Redirect() statement was causing something which was generating the ThreadAbortException.

The callstack gave some more information. Let's look at it. The Response.Redirect() internally executes some more methods in the following sequence:

Response.Redirect(url,endResponse)-->Response.End()-->Thread.Abort()-->Boom! (ThreadAbortException)

So, it appears the Response.Redirect() internally calls Resposne.End(), which calls Thread.Abort(), and this last method invocation causes the ThreadAbortException to be thrown.

ThreadAbort.png

Sequence of operations that causes ThreadAbortException

Why the ThreadAbortException is being thrown?

Well, a little research revealed to me that the Response.Redirect() is actually an instruction to the client browser to stop execution of the current page and hit a different URL request. As each and every request is being assigned to a different thread from the Thread Pool, the current thread (which is taking care of the current Request) should be terminated (and go back to the Thread Pool), and the next redirected Request should be handled by a different Thread Pool.

MSDN says:

"Thread.Abort() raises a ThreadAbortException in the thread on which it is invoked, to begin the process of terminating the thread. Calling this method usually terminates the thread."

ThreadAbortException.png

Child Thread raising ThreadAbortException to terminate itself

So, it turns out that when Response.Redirect() is being invoked, the current thread can't terminate itself, and hence it needs a way to notify the parent thread which created this thread (the ASP.NET worker process in this case) so that the parent thread can terminate this thread. Calling Thread.Abort() actually raises an Exception (ThreadAbortException) to the parent thread to notify that "I can't terminate myself, you please do it", and the parent thread terminates this thread and returns it back to the Thread Pool.

So this is the story behind the ThreadAbortException.

Can we avoid this ThreadAbortException?

Yes, we can! It's pretty simple actually. The Response.Redirect() has an overloaded version which lets you specify whether or not to end the Response while redirecting the current Request. Following is the overloaded version of the method:

Response.Redirect(string url, string endResponse);

So, if we invoke the method with endResposne=false, the Response.Redirect() will not call the Response.End(), and in turn, the Thread.Abort() method will not be called, and no ThreadAbortException will occur. Problem solved. Bravo!

I tried it, and unfortunately found that it stopped the ThreadAbortException but created a new problem. See the following piece of code (not the best piece of code you would ever see, but it demonstrates the problem):

public partial class _Default : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        if (!UserHasPermissionToViewThisPage())
        {
            Response.Redirect("Unauthorized.aspx",false);
        }

        DeleteFile();
    }

    private void DeleteFile()
    {
        string fileName = Server.MapPath("TextFile.txt");
        File.Delete(fileName);
    }

    private bool UserHasPermissionToViewThisPage()
    {
        return false;
    }
}

The code says, if the current user doesn't have permission to execute the functionality of this page, redirect him/her to "Unauthorized.aspx". On the other hand, if he/she has permission, execute the rest of the code for the current user and ultimately delete a file in the file system, in this particular case.

Put a text file (TextFile.txt) in the web root folder of your ASP.NET web site/application, copy/paste the above code in the code-behind file of a simple ASPX page, browse it, and see what happens.

As soon as you browse your page, you are being redirected to the Unauthorized.aspx page. Fine, this was expected. But where is TextFile.txt? OMG, it's gone!

It seems, even if Response.Redirect() is being executed here (and the browser is successfully being redirected to a new URL), the latter part of the codes is getting executed here. That is, the following code is being executed:

DeleteFile();

I changed the following Response.Redirect() statement (removed the endResponse parameter) and ran the page in the browser to see what happens (also restored a blank TextFile.txt file in the web root folder):

Response.Redirect("Unauthorized.aspx");

Interestingly, TextFile.txt is not deleted now, and obviously the DeleteFile(); statement is not being executed after the Response.Redirect() statement.

I gave a thought and concluded that whatever happening here with Response.Redirect(url, false) (note, the endResponse parameter is set to false) is logical. Why? Because, when I set the endResponse parameter value as false, Response.Redirect() doesn't call Response.End(), and hence Thread.Abort() is not being invoked here, resulting in the current thread (which is taking care of the current Request) being live and executing the other parts of the statements, whatever it does!

Dude, we have a problem here.

How to prevent the current thread from executing other code?

Simple, we just need to add a return statement just after the Response.Redirect() statement, right?

Response.Redirect("Unauthorized.aspx",false);
return;

Yes, this solves the problem. But, as I found, there are still issues with postbacks. The above code may return immediately after redirecting to a different URL, but if the current Request is a postback Request, the postback event methods are executed without any interruption. I added a Button server control to the page, added an OnClick event method to it, and, explored the issue. Take a look at the following code which demonstrates the problem:

public partial class _Default : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        if (IsPostBack)
        {
            if (!UserHasPermissionToViewThisPage())
            {
                Response.Redirect("Unauthorized.aspx", false);
                return;
            }
        }
        //This no longer gets called now
        DeleteFile();
    }

    private void DeleteFile()
    {
        string fileName = Server.MapPath("TextFile.txt");
        File.Delete(fileName);
    }

    private bool UserHasPermissionToViewThisPage()
    {
        return false;
    }

    protected void Button1_Click(object sender, EventArgs e)
    {
        //But, this is getting called even if Response.Redirect() is being called!
        DeleteFile();
    }
}

Further exploration revealed to me that, even if Response.Redirect(url, false) is supposed to halt the execution of the current page, the whole ASP.NET page life cycle events get executed here without any interruption. So, at the end of the life cycle, the Response is unnecessarily being written to the output stream even if the client browser is already redirected to a different URL.

So, it seems, returning just after the Response.Redirect() is not good enough. We need to track the redirection information somehow and prevent the execution of code within the postback event methods if the flag's value is true (indicating that the page has already redirected). We can use a global flag variable as follows:

bool Redirected = false;
...
...
if (!UserHasPermissionToViewThisPage())
{
   Response.Redirect("Unauthorized.aspx", false);
   Redirected = true;
   return;
}

protected void Button1_Click(object sender, EventArgs e)
{
    if(Redirected) return;
        DeleteFile();
} 

As it turns out, we need to check this flag in each and every postback event method in our code-behind methods. Doesn't sound good to me.

A smarter approach

The ASP.NET page life cycle is good. It gives you the flexibility to override most of the life-cycle events and execute your own piece of code where you want in specific parts of the life cycle. I might try to get rid of the fact that I need to check the flag inside each and every postback event method.

As the page life cycle says, the RaisePostBackEvent event is fired before firing the specific postback event method which is tied to a server control's specific event (in this case, the Button1_Click event method). So we can override this method and check the flag within the overridden method and let the ASP.NET runtime invoke the original ReaisePostBackEvent() method only if the flag's value is false.

Following is the code I wrote:

protected override void RaisePostBackEvent(IPostBackEventHandler sourceControl, 
                                           string eventArgument)
{
    if (Redirected == false)
        base.RaisePostBackEvent(sourceControl, eventArgument);
}

Adding to this, we also need to override the Render() event method so that the Response is not unnecessarily being written to the output stream when we call Response.Redirect(url, false):

protected override void Render(HtmlTextWriter writer)
{
    if (Redirected == false)
        base.Render(writer);
}

It would be even smarter to define these two methods and a Redirect() method within a base page class and let all other code-behind classes inherit this class. This would let us write less code and keep the original postback event methods clean. That is:

public class BasePage : System.Web.UI.Page
{
    protected bool Redirected = false;

    public BasePage()
    {
    }
  
    protected void Redirect(string url)
    {
        Response.Redirect(url, false);
        Redirected = true;
    }

    protected override void RaisePostBackEvent(IPostBackEventHandler 
                       sourceControl, string eventArgument)
    {
        if (Redirected == false)
            base.RaisePostBackEvent(sourceControl, eventArgument);
    }

    protected override void Render(HtmlTextWriter writer)
    {
        if (Redirected == false)
            base.Render(writer);
    } 
}

public partial class _Default : BasePage
{
    if (IsPostBack)
    {
            if (!UserHasPermissionToViewThisPage())
            {
                Redirect("Unauhorized.aspx");
                return;
            }
    }
    //This no longer gets called now
    DeleteFile();
   ....
}

To summarize, following are the things you have to do:

  • Create a BasePage and include the methods as suggested above. Inherit the BasePage from all other code-behind pages.
  • When you need to redirect, do not call Response.Redirect(). Instead, call the following piece of code:
  • base.Redirect(url);
    return;

Criticisms to this approach

Despite the fact that the above approach gives you a cleaner approach to avoiding ThreadAbortException from happening, there are a few unresolved issues. These are:

  • The current thread is not being aborted immediately. Rather, it stays alive, which is unnecessary.
  • To me, this is not a big problem because even though the thread isn't getting terminated immediately, it will execute and will be returned back to the ThreadPool as it completes the page life cycle (without really doing anything else, because the Redirected flag is true). So the ASP.NET worker process will not face thread starvation.

  • It enforces a constraint to write a return statement just after the base.Redirect() method, which may not be easy to follow everywhere.
  • To me, this is a big problem. Specially, writing just a return statement may not be a straightforward solution always, especially when you need to redirect from within a custom-written method which is not a page life cycle method. Take a look at the following piece of code:

    protected void Page_Load(object sender, EventArgs e)
    {
        if (!DenyIfUserDoesntHavePermissionToViewThisPage())
        {
            return;
        }
    }
    
    private bool DenyIfUserDoesntHavePermissionToViewThisPage()
    {
        bool userHavePermission = false;
    
        if (!userHavePermission)
        {
            base.Redirect("Unauthorized.aspx");
    
            return false;
        }
    
        return userHavePermission;
    }

    You see, we need to return false from within the custom method DenyIfUserDoesntHavePermissionToViewThisPage() (where we are redirecting to a different URL), and we also need to return false from within this method to the caller. If the caller method is being called from another method, we need to return false to that method also (from within the calling method), and we need to maintain this chain until we reach the page life cycle method which originally got called. This doesn't look comfortable.

  • It opens a risk of not writing the return statement after the base.Redirect() method at times, and this will result in execution of all other code which is written after the base.Redirect() statement.

To me, this is the biggest problem:

If you forget to write the return statement after calling the base.Redirect() method (which is a very easy mistake to do), disaster might happen. How would you deal with this risk? Unfortunately, I do not see any solution!

Enough! Tell me what to do

OK, I know I danced around the original problem for long enough, and if you had patience to read to this far, considering all the facts, let me tell you now how I think the Response.Redirect() should be handled in the best possible way:

  • Do not write any try..catch blocks in the code-behind. This will make sure you are not catching any ThreadAbortException there.
  • Redirect using the classic Response.Redirect(url) statement as you love to do Smile | :)
  • Simply ignore the ThreadAbortException in the Global.asax (where you usually catch your unhandled exceptions and report), using the following code:
  • void Application_Error(object sender, EventArgs e)
    { 
        // Code that runs when an un-handled error occurs
        Exception ex = Server.GetLastError();
        
        System.Threading.ThreadAbortException exception = 
               ex as System.Threading.ThreadAbortException;
        if (exception == null)
        {
             //Log or report only if this is not ThreadAbortException
             Logger.LogException(ex);   
        }
    }

    Another approach may be to simply swallow the ThreadAbortException in the BasePage's Redirect() method and call that method anywhere where there is a need to redirect.

    protected void Redirect(string url)
    {
        try
        {
            Response.Redirect(url);
        }
        catch 
        {
            //Swallow it!
        }
    }
    
    base.Redirect(); //Redirects to a different URL

Yes, I know some of you might scream and shout at me "What the hell are you saying? You are letting an Exception to be thrown and just eating it after catching". You know, exceptions are heavy and you shouldn't do this, and that...

Let me tell you, this is what I learned from my exploration of ThreadAbortException and Response.Redirect(). Suggest a better way if you can!

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Al-Farooque Shubho
Founder DropCue, SmartAspects
Bangladesh Bangladesh
I write codes to make life easier, and that pretty much describes me.
 
Sorry for not being able to contribute to CodeProject these days. I'be been busy with DropCue, which is not just another "to-do-list" or calendar management app, but an app to manage all of your "personal aspects" in one single place in such a simple, easy and innovative approach that no other system offers.
 
Give it a try, I bet you'll love it!
Follow on   Twitter

Comments and Discussions

 
GeneralMy vote of 1 [modified] PinmemberMichael Freidgeim1-Jun-13 1:08 
SuggestionCatching Thread Abort Exception PinmvpDave Kerr19-Apr-13 2:27 
GeneralMy vote of 5 Pinmembercsharpbd13-Nov-12 22:02 
Suggestionsimpler solution [modified] Pinmembergiammin29-May-12 22:55 
AnswerRe: simpler solution PinmemberHerbisaurus14-Aug-13 23:01 
QuestionRe: simpler solution Pinmembergiammin28-Aug-13 5:37 
GeneralMy vote of 5 Pinmembermanoj kumar choubey2-Apr-12 1:21 
GeneralMy vote of 5 PinmvpMd. Marufuzzaman9-Apr-11 20:26 
GeneralMy vote of 5 PinmembersT0Ps20-Mar-11 22:04 
GeneralRe: My vote of 5 PinmembersT0Ps20-Mar-11 22:12 
GeneralRe: My vote of 5 PinmemberPaulo Zemek31-May-11 4:30 
QuestionGood article, but why is it tagged for VB? PinmemberGregory.Gadow12-Mar-11 14:34 
AnswerRe: Good article, but why is it tagged for VB? PinmvpAl-Farooque Shubho12-Mar-11 19:06 
GeneralMy vote of 5 PinmemberAhmad Hyari10-Mar-11 3:38 
GeneralScanning the article and reading some of the responses it seems like a misunderstanding PinmemberZaibot6-Mar-11 13:05 
GeneralIgnoring ThreadAbortException is dangerous PinmemberGLLNS4-Mar-11 20:50 
GeneralRe: Ignoring ThreadAbortException is dangerous PinmvpAl-Farooque Shubho5-Mar-11 5:42 
GeneralMy vote of 5 PinmemberMonjurul Habib4-Mar-11 10:01 
GeneralRe: My vote of 5 Pinmemberfengyunfu4-Mar-11 19:29 
GeneralRe: My vote of 5 PinmemberMonjurul Habib4-Mar-11 20:48 
GeneralRe: My vote of 5 Pinmemberfengyunfu6-Mar-11 17:49 
GeneralMy vote of 2 PinmemberPaulo Zemek4-Mar-11 5:44 
GeneralRe: My vote of 2 [modified] PinmvpAl-Farooque Shubho4-Mar-11 18:40 
GeneralRe: My vote of 2 PinmemberPaulo Zemek7-Mar-11 6:17 
GeneralRe: My vote of 2 PinmvpAl-Farooque Shubho7-Mar-11 15:36 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web02 | 2.8.140721.1 | Last Updated 30 May 2011
Article Copyright 2011 by Al-Farooque Shubho
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid