Search code examples
asp.net-mvctempdata

TempData is empty when set in a catch block and we rethrow


I am using custom errors (web.config) to send unhandled exceptions to my ErrorController.

In my main Controller I throw an exception, catch it in a catch block and set the TempData to have some user-friendly message. I then throw which is then sent to the ErrorController.

When I check the TempData here, it is empty (as is the Session but it would be, wouldn't it?).

I am sure it is not meant to be empty, and this is the right way of sending data between controllers... but it is not working! Am I mistaken in my understanding?

HomeController:

catch (Exception ex)
{
    TempData["MsgForUser"] = "useful message to show";
    // Do some logging...

    throw;
}

ErrorController:

public ActionResult DisplayError(int id)
{
    ViewBag.MsgForUser = TempData["MsgForUser"];
    return View();
}

Web.config:

<system.web>
  <customErrors mode="On" defaultRedirect="~/ErrorPage/DisplayError">
    <error redirect="~/Error/DisplayError/403" statusCode="403" />
    <error redirect="~/Error/DisplayError/404" statusCode="404" />
    <error redirect="~/Error/DisplayError/500" statusCode="500" />
  </customErrors>

Solution

  • I found that the session was being destroyed upon doing a throw in the top-level catch block, because the request to the error controller. However I did find the returning RedirectToAction() preserved it.

    So, I created my own exception type:

    public class MyApplicationException : Exception
    {
        /// <summary>
        /// A message that can be shown to the user i.e. not technical
        /// </summary>
        public string MessageForUser { get; set; }
    
        public MyApplicationException()
        {
        }
    
        public MyApplicationException(string message, string messageForUser="")
            : base(message)
        {
            MessageForUser = messageForUser;
        }
    
        public MyApplicationException(string message, Exception inner, string messageForUser = "")
            : base(message, inner)
        {
            MessageForUser = messageForUser;
        }
    }
    

    then when I encounter a problem that I am expecting and will end up sending the user to a generic error page, I use it like so:

    if (MyList.Count > 14)
    {
          throw new MyApplicationException("More than 14 records received: " + MyList.Count.ToString(), "There is a maximum limit of 14 records that can be processed at a time. Please call us to discuss your requirement to process in greater quantities.");
    }
    

    and upon catching it I pick out the message for the user, log the error, and return RedirectToAction() to go to the error page without losing the TempData.

    catch (MyApplicationException myAppEx)
    {
        // If there is a user-friendly message then store it for DisplayError to show
        if (String.IsNullOrEmpty(myAppEx.MessageForUser) == false)
        {
            TempData["MsgForUser"] = myAppEx.MessageForUser;
        }
    
        // Do some logging to elmah or a custom logger
        util.LogError(myAppEx, "MyFunc() threw an exception", args);
    
        //
        // We cannot throw here because we lose the Session & TempData. we have to do a manual redirect.
        //
        return RedirectToAction("DisplayError", "ErrorPage", new { id = 500 });
    }
    

    and finally in my view, I look for it and display if it exists:

    <p>
        An error occurred. Please check & try again later.
    </p>
    @if (String.IsNullOrEmpty(ViewBag.MsgForUser) == false)
    {
        <p class="more-info">Details: @ViewBag.MsgForUser</p>
    }