You might create a custom IControllerFactory, e.g. by deriving from DefaultControllerFactory.
Its GetControllerInstance method will be called with a null value for the controllerType argument, when no matching controller could be resolved.
At this point, you are ahead of the Exception.
IController GetControllerInstance(RequestContext requestContext, Type controllerType)
Here you decide how to handle the request, like e.g. logging and/or handling the request by a specific controller.
The example below shows how the ErrorController is being used as fallback, executing its Index action method.
class CustomControllerFactory : DefaultControllerFactory
{
    protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType)
    {
        if (controllerType == null)
        {
            var routeData = requestContext.RouteData;
            routeData.Values["controller"] = "Error";
            routeData.Values["action"] = "Index";
            controllerType = typeof(ErrorController);
        }
        return base.GetControllerInstance(requestContext, controllerType);
    }
}
A custom controller factory gets installed from within e.g. Application_Start in Global.asax.cs using:
ControllerBuilder.Current.SetControllerFactory(new CustomControllerFactory());
Note that this doesn't guard you from a related exception in case the contoller does exist, but the action doesn't.
E.g. given a HomeController without an Ufo action method, the url www.myapp.com/home/ufo results in an Exception "A public action method 'ufo' was not found on controller '...HomeController".
A simple solution to handle such scenario is by overriding HandleUnknownAction in a custom base controller of which all your controllers inherit.
The example below shows how a shared Error view (Shared\Error.cshtml) gets executed.
public abstract class BaseController : Controller
{
    protected override void HandleUnknownAction(string actionName)
    {
        View("Error").ExecuteResult(ControllerContext);
    }
}