I've investigated A LOT on how to properly manage 404s in MVC (specifically MVC3), and this, IMHO is the best solution I've come up with:
In global.asax:
public class MvcApplication : HttpApplication
{
    protected void Application_EndRequest()
    {
        if (Context.Response.StatusCode == 404)
        {
            Response.Clear();
            var rd = new RouteData();
            rd.DataTokens["area"] = "AreaName"; // In case controller is in another area
            rd.Values["controller"] = "Errors";
            rd.Values["action"] = "NotFound";
            IController c = new ErrorsController();
            c.Execute(new RequestContext(new HttpContextWrapper(Context), rd));
        }
    }
}
ErrorsController:
public sealed class ErrorsController : Controller
{
    public ActionResult NotFound()
    {
        ActionResult result;
        object model = Request.Url.PathAndQuery;
        if (!Request.IsAjaxRequest())
            result = View(model);
        else
            result = PartialView("_NotFound", model);
        return result;
    }
}
Edit:
If you're using IoC (e.g. AutoFac), you should create your controller using:
var rc = new RequestContext(new HttpContextWrapper(Context), rd);
var c = ControllerBuilder.Current.GetControllerFactory().CreateController(rc, "Errors");
c.Execute(rc);
Instead of
IController c = new ErrorsController();
c.Execute(new RequestContext(new HttpContextWrapper(Context), rd));
(Optional)
Explanation:
There are 6 scenarios that I can think of where an ASP.NET MVC3 apps can generate 404s.
Generated by ASP.NET:
- Scenario 1: URL does not match a route in the route table.
 
Generated by ASP.NET MVC:
Scenario 2: URL matches a route, but specifies a controller that doesn't exist.
 
Scenario 3: URL matches a route, but specifies an action that doesn't exist.
 
Manually generated:
Scenario 4: An action returns an HttpNotFoundResult by using the method HttpNotFound().
 
Scenario 5: An action throws an HttpException with the status code 404.
 
Scenario 6: An actions manually modifies the Response.StatusCode property to 404.
 
Objectives
(A) Show a custom 404 error page to the user.
 
(B) Maintain the 404 status code on the client response (specially important for SEO).
 
(C) Send the response directly, without involving a 302 redirection.
 
Solution Attempt: Custom Errors
<system.web>
    <customErrors mode="On">
        <error statusCode="404" redirect="~/Errors/NotFound"/>
    </customErrors>
</system.web>
Problems with this solution:
- Does not comply with objective (A) in scenarios (1), (4), (6).
 
- Does not comply with objective (B) automatically. It must be programmed manually.
 
- Does not comply with objective (C).
 
Solution Attempt: HTTP Errors
<system.webServer>
    <httpErrors errorMode="Custom">
        <remove statusCode="404"/>
        <error statusCode="404" path="App/Errors/NotFound" responseMode="ExecuteURL"/>
    </httpErrors>
</system.webServer>
Problems with this solution:
- Only works on IIS 7+.
 
- Does not comply with objective (A) in scenarios (2), (3), (5).
 
- Does not comply with objective (B) automatically. It must be programmed manually.
 
Solution Attempt: HTTP Errors with Replace
<system.webServer>
    <httpErrors errorMode="Custom" existingResponse="Replace">
        <remove statusCode="404"/>
        <error statusCode="404" path="App/Errors/NotFound" responseMode="ExecuteURL"/>
    </httpErrors>
</system.webServer>
Problems with this solution:
- Only works on IIS 7+.
 
- Does not comply with objective (B) automatically. It must be programmed manually.
 
- It obscures application level http exceptions. E.g. can't use customErrors section, System.Web.Mvc.HandleErrorAttribute, etc. It can't only show generic error pages.
 
Solution Attempt customErrors and HTTP Errors
<system.web>
    <customErrors mode="On">
        <error statusCode="404" redirect="~/Errors/NotFound"/>
    </customError>
</system.web>
and
<system.webServer>
    <httpErrors errorMode="Custom">
        <remove statusCode="404"/>
        <error statusCode="404" path="App/Errors/NotFound" responseMode="ExecuteURL"/>
    </httpErrors>
</system.webServer>
Problems with this solution:
- Only works on IIS 7+.
 
- Does not comply with objective (B) automatically. It must be programmed manually.
 
- Does not comply with objective (C) in scenarios (2), (3), (5).
 
People that have troubled with this before even tried to create their own libraries (see http://aboutcode.net/2011/02/26/handling-not-found-with-asp-net-mvc3.html). But the previous solution seems to cover all the scenarios without the complexity of using an external library.