Refactor exception handling to return instead of echoing.

By returning a response/string instead of directly echoing we get a few benefits:

1. We can more easily test the code without relying on ob_start().
2. Application exception handlers can have full control over the response headers.
3. We can fix issues related to nested error pages.

Fixes #4130