599

I can use set_error_handler() to catch most PHP errors, but it doesn't work for fatal (E_ERROR) errors, such as calling a function that doesn't exist. Is there another way to catch these errors?

I am trying to call mail() for all errors and am running PHP 5.2.3.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
too much php
  • 88,666
  • 34
  • 128
  • 138
  • **See also:** https://stackoverflow.com/questions/1087365/php-try-catch-blocks-are-they-able-to-catch-invalid-arg-types – dreftymac Mar 15 '18 at 20:42
  • **See also:** https://bugs.php.net/bug.php?id=41418 – dreftymac Jul 26 '18 at 17:14
  • **See also:** https://stackoverflow.com/questions/7116995 – dreftymac Nov 12 '18 at 21:54
  • 1
    I wrote up a wiki-style Q&A with a complete solution for catching All errors in PHP; which can be viewed/gleaned/stolen/critiqued [here on Stack Overflow](https://stackoverflow.com/questions/27238473/how-can-i-catch-all-errors-in-php/27238474#27238474). The solution includes five methods that wrap all errors PHP can generate that will eventually pass said errors up to an 'ErrorHandler' typed object. – DigitalJedi805 Dec 01 '14 at 19:36
  • I provided a simple answer that works for PHP 7: https://stackoverflow.com/questions/7116995/is-it-possible-in-php-to-prevent-fatal-error-call-to-undefined-function – David Spector Apr 30 '21 at 17:11

18 Answers18

669

Log fatal errors using the register_shutdown_function, which requires PHP 5.2+:

register_shutdown_function( "fatal_handler" );

function fatal_handler() {
    $errfile = "unknown file";
    $errstr  = "shutdown";
    $errno   = E_CORE_ERROR;
    $errline = 0;

    $error = error_get_last();

    if($error !== NULL) {
        $errno   = $error["type"];
        $errfile = $error["file"];
        $errline = $error["line"];
        $errstr  = $error["message"];

        error_mail(format_error( $errno, $errstr, $errfile, $errline));
    }
}

You will have to define the error_mail and format_error functions. For example:

function format_error( $errno, $errstr, $errfile, $errline ) {
    $trace = print_r( debug_backtrace( false ), true );

    $content = "
    <table>
        <thead><th>Item</th><th>Description</th></thead>
        <tbody>
            <tr>
                <th>Error</th>
                <td><pre>$errstr</pre></td>
            </tr>
            <tr>
                <th>Errno</th>
                <td><pre>$errno</pre></td>
            </tr>
            <tr>
                <th>File</th>
                <td>$errfile</td>
            </tr>
            <tr>
                <th>Line</th>
                <td>$errline</td>
            </tr>
            <tr>
                <th>Trace</th>
                <td><pre>$trace</pre></td>
            </tr>
        </tbody>
    </table>";
    return $content;
}

Use Swift Mailer to write the error_mail function.

See also:

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
user259973
  • 6,739
  • 1
  • 15
  • 2
  • 119
    +1 This is the *actual* correct answer. I don't know why people are getting hung up on "you cannot recover from fatal errors"--the question didn't say anything about recovering. – David Harkness Nov 01 '11 at 21:53
  • 21
    Thanks, good one. Recovering from fatal errors (memory limits for example) is not something that I would try to do, but making these errors discoverable (without customer submitting a support ticket) makes all the difference. – Ilija Feb 11 '12 at 10:51
  • 1
    it catch also parse errors if that is not in the same file of register function declaration. (assuming this declaration in main file) – Luca Rainone Aug 21 '12 at 12:37
  • 1
    Note that the callback function needs to go before the call to register_shutdown_function(). – taco Sep 24 '12 at 15:23
  • 1
    @SlavaN the variables are set before the if, so they will just email that the error was unknown if an error could not be set. – Scott Nicol Jun 07 '13 at 11:27
  • 3
    Using basic mail: `mail("myname@myemail.com", "My Site: FATAL ERROR", "Details: " . $errno . ' ' . $errstr . ' ' . $errfile . ' ' . $errline);` – Eric Jun 17 '13 at 07:02
  • 4
    @ScottNicol Slava V is correct, because the shutdown function is called every time the script finishes running. With the way the code is written now, an email will be sent on EVERY page load. – Nate Jan 21 '14 at 03:46
  • Note that `register_shutdown_function` is called before destructors, so if a destructor throws an exception, it will not be handled by your callback. – Alain Tiemblo Oct 23 '14 at 11:32
  • Minor nitpick, but for the sake of brevity, since you are expecting an array, you could just do `if($error)` instead of `if( $error !== NULL)`. – Mahn Nov 09 '14 at 13:18
  • Note that (1) `register_shutdown_function` appends the given function to a shutdown callbacks queue, so it must play nicely with unknown code executed before and after, during the same shutdown; (2) it does not check whether the given function is already in the queue or not; (3) it's not possible to un-register a registered function. Therefore, a registered shutdown function, not only must play nicely with other unknown code, but also with possible additional executions of its own code. Tip: Use something like `static $count; ... $count++; ... if ($count > 1) return; ... (fatal handling) `. – aercolino Jan 15 '15 at 20:50
  • 1
    LOL ! 480 upvotes? for what? the example you gave: ` $trace = print_r( debug_backtrace( false ), true );` can and will deadlock the serverscript! it did so to me. Yes yes, not necessarily. but eventually it will. As this loop is not limited and you do not know what you are doing. Avoid this at all cost unless you like restarting your apache server. – Toskan Jun 02 '16 at 22:22
  • Since the `error_mail` is within the if statement, the default variable values are never used. Can be safely removed... – Stanimir Stoyanov Jun 29 '17 at 13:25
  • 5
    Note: this is not a 100% correct answer. Any place that uses an @ symbol to ignore errors will still SET the last error (so you can handle errors). So your script finishes without a problem but the register_shutdown_function still thinks an error happened. Only since PHP 7 though have they had a function error_clear_last(). – Rahly Aug 24 '17 at 23:56
  • debug_backtrace shows only "fatal_handler" stack and not the stack when the error occurred. I wonder if there is a way to see the stack when the fatal error occurred. – user34814 Mar 17 '20 at 08:44
  • 1
    The `if($error !== NULL)` here is not a strong enough check - `error_get_last()` should perhaps be called "diagnostic_get_last()", as it will also return the most recent Warning, Notice, or even Deprecation. If you want to detect fatal errors, you **must** check `$error['type']` as in [periklis' answer](https://stackoverflow.com/a/3389021/157957). – IMSoP Feb 08 '22 at 19:12
170

I just came up with this solution (PHP 5.2.0+):

function shutDownFunction() {
    $error = error_get_last();
     // Fatal error, E_ERROR === 1
    if ($error['type'] === E_ERROR) {
         // Do your stuff
    }
}
register_shutdown_function('shutDownFunction');

Different error types are defined at Predefined Constants.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
periklis
  • 10,102
  • 6
  • 60
  • 68
  • 31
    This solution does much more for me than top rated answer. The top-rated answer will send you an email every time the script runs, even if there is no error. This one strictly runs on a fatal error. – kmoney12 Mar 26 '13 at 01:04
  • 1
    @periklis, if the last error was already handled, error_get_last would still return it wouldn't it? – Pacerier Jul 12 '13 at 12:49
  • @Pacerier I'm not sure what you mean with "handled", as errors are not exceptions, but I suppose the answer is "yes" – periklis Jul 12 '13 at 13:10
  • @periklis, I mean you have already "handled" the previous error using the error handler in set_error_handler. In this case, you would be "handling" that error again when the script shuts down. – Pacerier Jul 12 '13 at 13:19
  • 4
    @Pacerier I see, that's an interesting question. Have a look at http://www.php.net/error_get_last, one of the comments mentions that "`If an error handler (see set_error_handler ) successfully handles an error then that error will not be reported by this function.`" – periklis Jul 12 '13 at 13:38
  • @periklis, he wasn't being exact. In set_error_handler, sometimes we make our handler return false to allow default behavior (normal error handling) to continue. In this case, error_get_last would screw up by reporting the same error again. What's a better solution? – Pacerier Jul 12 '13 at 14:36
  • If you are referring to FATAL errors, as the OP's original question, then your `set_error_handler()` will not be executed. There are specific error types that `set_error_handler()` can handle. I would suggest that you use `register_shutdown_function()` for all the rest (see the manual for a complete list) – periklis Jul 13 '13 at 05:13
  • 1
    Perhaps this is obvious, calling `register_shutdown_function()` must be earlier than any fatal error. `use_1T_memory(); /* memory exhausted error here! */ register_shutdown_function('shutDownFunction');` won't work as expected. – Nobu Mar 25 '16 at 23:32
  • In addition to E_ERROR you'll probably want to check for E_COMPILE_ERROR, E_CORE_ERROR, and maybe even E_PARSE. As others have said any function registered with register_shutdown_function() will be run on every call to the script so you'll want to keep whatever code you put in such a function to a minimum and not do things like try to send headers without testing with headers_sent() or anything else that may cause additional errors. – Night Owl May 16 '16 at 05:33
  • if there were no errors, code: if ($error['type'] === E_ERROR) will cause warning, because you are trying to acces non existing key in NULL – Jimmmy Jan 20 '17 at 08:41
  • What if another fatal error is thrown inside the shutdown function? Will the code enter an infinite loop? – HomeIsWhereThePcIs Jul 16 '20 at 09:48
122

PHP doesn't provide conventional means for catching and recovering from fatal errors. This is because processing should not typically be recovered after a fatal error. String matching an output buffer (as suggested by the original post the technique described on PHP.net) is definitely ill-advised. It's simply unreliable.

Calling the mail() function from within an error handler method prove to be problematic, too. If you had a lot of errors, your mail server would be loaded with work, and you could find yourself with a gnarly inbox. To avoid this, you might consider running a cron to scan error logs periodically and send notifications accordingly. You might also like to look into system monitoring software, such as Nagios.


To speak to the bit about registering a shutdown function:

It's true that you can register a shutdown function, and that's a good answer.

The point here is that we typically shouldn't try to recover from fatal errors, especially not by using a regular expression against your output buffer. I was responding to the accepted answer, which linked to a suggestion on php.net which has since been changed or removed.

That suggestion was to use a regex against the output buffer during exception handling, and in the case of a fatal error (detected by the matching against whatever configured error text you might be expecting), try to do some sort of recovery or continued processing. That would not be a recommended practice (I believe that's why I can't find the original suggestion, too. I'm either overlooking it, or the php community shot it down).

It might be worth noting that the more recent versions of PHP (around 5.1) seem to call the shutdown function earlier, before the output buffering callback is envoked. In version 5 and earlier, that order was the reverse (the output buffering callback was followed by the shutdown function). Also, since about 5.0.5 (which is much earlier than the questioner's version 5.2.3), objects are unloaded well before a registered shutdown function is called, so you won't be able to rely on your in-memory objects to do much of anything.

So registering a shutdown function is fine, but the sort of tasks that ought to be performed by a shutdown function are probably limited to a handful of gentle shutdown procedures.

The key take-away here is just some words of wisdom for anyone who stumbles upon this question and sees the advice in the originally accepted answer. Don't regex your output buffer.

Community
  • 1
  • 1
keparo
  • 33,450
  • 13
  • 60
  • 66
  • 28
    Pfff, I remember those 650.000+ e-mails i got the following morning. Since then my ErrorHandler is capped at 100 emails per webserver. – Bob Fanger Sep 23 '09 at 08:12
  • 15
    That's not true. You can capture fatal errors with register_shutdown_function. – hipertracker Sep 25 '10 at 20:24
  • 59
    There do exist use cases for wanting to catch fatal errors. Test suites, for example, shouldn't just stop when one fails, they should report the fatal error and go on to the next test. PHP just makes too many things "fatal" errors. – Chad Apr 19 '11 at 20:46
  • 26
    Yeah saying they "shouldn't be caught" is very short sighted. In a production system, you *need* to know when something fails (set up emails or log things in a database - default php error handling is not very sophisticated). – B T May 09 '11 at 21:28
  • 2
    Yes, they "shouldn't be caught", but unfortunately errors are often used in places where exceptions should be (poor quality libraries, poor quality projects). This makes me sad. – Benbob Jul 10 '11 at 23:59
  • 9
    I want to make a quick comment about what you are all saying about "Errors need to be caught, so that we can fix them"...Ini directives ini log_errors and error_log. – Kelly Elton Oct 26 '11 at 22:26
  • 3
    To extend on that, I've very RARELY had a fatal error. I mean you can call a function or variable that doesn't exist, and it still keeps on chugging along. This is also why it's important to test your code before it hits production. – Kelly Elton Oct 26 '11 at 22:30
  • 1
    You could also toss a custom number into the header, like say 400(just an example, this number wouldn't be the best choice). Then your log has the error, and apache spits out the correct error page based on the number. – Kelly Elton Oct 26 '11 at 22:36
  • You might also want to check this out http://code.google.com/p/lagger/ you can even get extensions for say Chrome, so that you can view your log ctrl+i and see errors and such. So they arn't visible on the page, but you can still accessing them without having to dig through log files. – Kelly Elton Oct 26 '11 at 22:39
  • 3
    This is wrong. It's perfectly normal and correct to catch fatal errors, so you can do things like log them and perform cleanup. PHP's error handling is just poorly designed. – Glenn Maynard May 17 '12 at 18:03
  • Running php 5.3.3-7+squeeze13, I disagree with "objects are unloaded well before a registered shutdown function is called". I accessed my objects in a registered shutdown function and it worked fine, all objects have the same data they did before the error. – Simon Forsberg Jul 01 '12 at 20:54
  • 3
    Every other language javascript, c# etc lets you catch fatals with try/catch. PHP is just plain wrong here. – Matthew Lock Sep 18 '13 at 10:12
  • 4
    Some fatal errors shouldn't be caught but the vast majority of php fatals errors should just be exceptions! Calling a method on a null variable should be an exception! Unsupported operand types should be an exception! PHP basically forces you to do LBYL programming in those cases which is inconsistent. – Jeff Whiting Dec 12 '13 at 19:07
  • 1
    Personally I think ALL error should be caught. Why? #1 system will be running in front of a user, so you might want to put all errors into a database and/or email a developer. Errors that are logged in a flat text file offer little to almost useless information, no information on what user caused it, nor how the php script was called. #2 Depending on how it was called (i.e. ajax vs straight html) you may want to output a different format (json/xml) than always HTML. – Rahly May 11 '14 at 03:23
  • That's right. The point is that errors should be caught, but fatal errors should not be caught in order to somehow recover, using odd and unreliable tactics (like applying a regular expression to the output buffer). – keparo Jul 01 '15 at 01:15
65

Fatal errors or recoverable fatal errors now throw instances of Error in PHP 7 or higher versions. Like any other exceptions, Error objects can be caught using a try/catch block.

Example:

<?php
$variable = 'not an object';

try {
    $variable->method(); // Throws an Error object in PHP 7 or higger.
} catch (Error $e) {
    // Handle error
    echo $e->getMessage(); // Call to a member function method() on string
}

https://3v4l.org/67vbk

Or you can use Throwable interface to catch all exceptions.

Example:

<?php
    try {
        undefinedFunctionCall();
    } catch (Throwable $e) {
        // Handle error
        echo $e->getMessage(); // Call to undefined function undefinedFunctionCall()
    }

https://3v4l.org/Br0MG

For more information: http://php.net/manual/en/language.errors.php7.php

LugiHaue
  • 2,702
  • 1
  • 15
  • 13
  • 3
    Any ideia on how to use this to catch an error like `Fatal error: Trait 'FailedTrait' not found in` when using `ReflectionClass`? – TCB13 Apr 20 '18 at 11:41
  • 2
    @TCB13 try to wrap the try inner contents in a file and `include "filename.php"` instead in the `try` block, then `Throwable` catch block at least works for `ParseError`. – Niloct Nov 01 '18 at 18:09
  • in PHP does it matter if you use, small-letter e for exception, or does it have to be big-letter E for Exception? – kstubs Sep 15 '21 at 20:00
  • 1
    This is true for _most_ errors, but there are still some which cause an immediate shutdown, such as running out of memory. For those, the shutdown handler is still the only graceful way to emit a custom response. – IMSoP Feb 08 '22 at 19:14
  • It does not work for `Declaration of ... must be compatible with...` even in PHP 8.0 – Yevgeniy Afanasyev May 25 '22 at 08:47
37

Well, it seems possible to catch fatal errors some other way :)

ob_start('fatal_error_handler');

function fatal_error_handler($buffer){
    $error = error_get_last();
    if($error['type'] == 1){
        // Type, message, file, line
        $newBuffer='<html><header><title>Fatal Error </title></header>
                      <style>
                    .error_content{
                        background: ghostwhite;
                        vertical-align: middle;
                        margin:0 auto;
                        padding: 10px;
                        width: 50%;
                     }
                     .error_content label{color: red;font-family: Georgia;font-size: 16pt;font-style: italic;}
                     .error_content ul li{ background: none repeat scroll 0 0 FloralWhite;
                                border: 1px solid AliceBlue;
                                display: block;
                                font-family: monospace;
                                padding: 2%;
                                text-align: left;
                      }
                      </style>
                      <body style="text-align: center;">
                        <div class="error_content">
                             <label >Fatal Error </label>
                             <ul>
                               <li><b>Line</b> ' . $error['line'] . '</li>
                               <li><b>Message</b> ' . $error['message'] . '</li>
                               <li><b>File</b> ' . $error['file'] . '</li>
                             </ul>

                             <a href="javascript:history.back()"> Back </a>
                        </div>
                      </body></html>';

        return $newBuffer;
    }
    return $buffer;
}
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
sakhunzai
  • 13,900
  • 23
  • 98
  • 159
  • 3
    I would give this 10 upvotes if I could. It works perfectly for me on those odd errors that sometimes occur when a page bombs and nothing is being logged. I wouldn't use in live production code but it's great to add to a page when a quick answer to what is failing is needed. Thank you! – Night Owl Jan 14 '12 at 05:35
  • One of the best solutions I've found on the Internet. Works like charm. – Bounce Feb 11 '13 at 22:56
  • 2
    In what way? An explanation would be in order, especially if it is one of the best solutions on the Internet (it could become even better). – Peter Mortensen Apr 21 '20 at 01:33
  • Is e.g. all the CSS content needed? Couldn't it be cut down to the essentials? Respond by editing your answer, not here in comments (as appropriate). – Peter Mortensen Apr 21 '20 at 01:35
  • @PeterMortensen I dont claim its best. Also its my personal solution to problem , there are other better options much professional ones. As suggested by someone its not good fit for production. Css is there bcz I just cut-pasted my personal code – sakhunzai Apr 22 '20 at 09:03
  • @PeterMortensen also there is no single solution to this problem , it might have got upvotes bcz it addresses the problem they are facing not bcz its best – sakhunzai Apr 22 '20 at 09:06
30

You can't catch/handle fatal errors, but you can log/report them. For quick debugging I modified one answer to this simple code

function __fatalHandler()
{
    $error = error_get_last();

    // Check if it's a core/fatal error, otherwise it's a normal shutdown
    if ($error !== NULL && in_array($error['type'],
        array(E_ERROR, E_PARSE, E_CORE_ERROR, E_CORE_WARNING,
              E_COMPILE_ERROR, E_COMPILE_WARNING,E_RECOVERABLE_ERROR))) {

        echo "<pre>fatal error:\n";
        print_r($error);
        echo "</pre>";
        die;
    }
}

register_shutdown_function('__fatalHandler');
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
zainengineer
  • 13,289
  • 6
  • 38
  • 28
28

I developed a way to catch all error types in PHP (almost all)! I have no sure about E_CORE_ERROR (I think will not works only for that error)! But, for other fatal errors (E_ERROR, E_PARSE, E_COMPILE...) works fine using only one error handler function! There goes my solution:

Put this following code on your main file (index.php):

<?php
    define('E_FATAL',  E_ERROR | E_USER_ERROR | E_PARSE | E_CORE_ERROR |
            E_COMPILE_ERROR | E_RECOVERABLE_ERROR);

    define('ENV', 'dev');

    // Custom error handling vars
    define('DISPLAY_ERRORS', TRUE);
    define('ERROR_REPORTING', E_ALL | E_STRICT);
    define('LOG_ERRORS', TRUE);

    register_shutdown_function('shut');

    set_error_handler('handler');

    // Function to catch no user error handler function errors...
    function shut(){

        $error = error_get_last();

        if($error && ($error['type'] & E_FATAL)){
            handler($error['type'], $error['message'], $error['file'], $error['line']);
        }

    }

    function handler( $errno, $errstr, $errfile, $errline ) {

        switch ($errno){

            case E_ERROR: // 1 //
                $typestr = 'E_ERROR'; break;
            case E_WARNING: // 2 //
                $typestr = 'E_WARNING'; break;
            case E_PARSE: // 4 //
                $typestr = 'E_PARSE'; break;
            case E_NOTICE: // 8 //
                $typestr = 'E_NOTICE'; break;
            case E_CORE_ERROR: // 16 //
                $typestr = 'E_CORE_ERROR'; break;
            case E_CORE_WARNING: // 32 //
                $typestr = 'E_CORE_WARNING'; break;
            case E_COMPILE_ERROR: // 64 //
                $typestr = 'E_COMPILE_ERROR'; break;
            case E_CORE_WARNING: // 128 //
                $typestr = 'E_COMPILE_WARNING'; break;
            case E_USER_ERROR: // 256 //
                $typestr = 'E_USER_ERROR'; break;
            case E_USER_WARNING: // 512 //
                $typestr = 'E_USER_WARNING'; break;
            case E_USER_NOTICE: // 1024 //
                $typestr = 'E_USER_NOTICE'; break;
            case E_STRICT: // 2048 //
                $typestr = 'E_STRICT'; break;
            case E_RECOVERABLE_ERROR: // 4096 //
                $typestr = 'E_RECOVERABLE_ERROR'; break;
            case E_DEPRECATED: // 8192 //
                $typestr = 'E_DEPRECATED'; break;
            case E_USER_DEPRECATED: // 16384 //
                $typestr = 'E_USER_DEPRECATED'; break;
        }

        $message =
            '<b>' . $typestr .
            ': </b>' . $errstr .
            ' in <b>' . $errfile .
            '</b> on line <b>' . $errline .
            '</b><br/>';

        if(($errno & E_FATAL) && ENV === 'production'){

            header('Location: 500.html');
            header('Status: 500 Internal Server Error');

        }

        if(!($errno & ERROR_REPORTING))
            return;

        if(DISPLAY_ERRORS)
            printf('%s', $message);

        //Logging error on php file error log...
        if(LOG_ERRORS)
            error_log(strip_tags($message), 0);
    }

    ob_start();

    @include 'content.php';

    ob_end_flush();
?>
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Lucas Batistussi
  • 2,283
  • 3
  • 27
  • 35
17

You cannot throw an exception inside a registered shutdown function like that:

<?php
    function shutdown() {
        if (($error = error_get_last())) {
           ob_clean();
           throw new Exception("fatal error");
        }
    }

    try {
        $x = null;
        $x->method()
    } catch(Exception $e) {
        # This won't work
    }
?>

But you can capture and redirect request to another page.

<?php
    function shutdown() {
        if (($error = error_get_last())) {
           ob_clean();
           # Report the event, send email, etc.
           header("Location: http://localhost/error-capture");
           # From /error-capture. You can use another
           # redirect, to e.g. the home page
        }
    }
    register_shutdown_function('shutdown');

    $x = null;
    $x->method()
?>
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
hipertracker
  • 2,425
  • 26
  • 16
15

If you are using PHP >= 5.1.0 Just do something like this with the ErrorException class:

<?php
    // Define an error handler
    function exception_error_handler($errno, $errstr, $errfile, $errline ) {
        throw new ErrorException($errstr, $errno, 0, $errfile, $errline);
    }

    // Set your error handler
    set_error_handler("exception_error_handler");

    /* Trigger exception */
    try
    {
        // Try to do something like finding the end of the internet
    }
    catch(ErrorException $e)
    {
        // Anything you want to do with $e
    }
?>
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Cyril Tata
  • 199
  • 1
  • 3
10

Nice solution found in Zend Framework 2:

/**
 * ErrorHandler that can be used to catch internal PHP errors
 * and convert to an ErrorException instance.
 */
abstract class ErrorHandler
{
    /**
     * Active stack
     *
     * @var array
     */
    protected static $stack = array();

    /**
     * Check if this error handler is active
     *
     * @return bool
     */
    public static function started()
    {
        return (bool) static::getNestedLevel();
    }

    /**
     * Get the current nested level
     *
     * @return int
     */
    public static function getNestedLevel()
    {
        return count(static::$stack);
    }

    /**
     * Starting the error handler
     *
     * @param int $errorLevel
     */
    public static function start($errorLevel = \E_WARNING)
    {
        if (!static::$stack) {
            set_error_handler(array(get_called_class(), 'addError'), $errorLevel);
        }

        static::$stack[] = null;
    }

    /**
     * Stopping the error handler
     *
     * @param  bool $throw Throw the ErrorException if any
     * @return null|ErrorException
     * @throws ErrorException If an error has been catched and $throw is true
     */
    public static function stop($throw = false)
    {
        $errorException = null;

        if (static::$stack) {
            $errorException = array_pop(static::$stack);

            if (!static::$stack) {
                restore_error_handler();
            }

            if ($errorException && $throw) {
                throw $errorException;
            }
        }

        return $errorException;
    }

    /**
     * Stop all active handler
     *
     * @return void
     */
    public static function clean()
    {
        if (static::$stack) {
            restore_error_handler();
        }

        static::$stack = array();
    }

    /**
     * Add an error to the stack
     *
     * @param int    $errno
     * @param string $errstr
     * @param string $errfile
     * @param int    $errline
     * @return void
     */
    public static function addError($errno, $errstr = '', $errfile = '', $errline = 0)
    {
        $stack = & static::$stack[count(static::$stack) - 1];
        $stack = new ErrorException($errstr, 0, $errno, $errfile, $errline, $stack);
    }
}

This class allows you to start the specific ErrorHandler sometimes if you need it. And then you can also stop the Handler.

Use this class e.g. like this:

ErrorHandler::start(E_WARNING);
$return = call_function_raises_E_WARNING();

if ($innerException = ErrorHandler::stop()) {
    throw new Exception('Special Exception Text', 0, $innerException);
}

// or
ErrorHandler::stop(true); // directly throws an Exception;

Link to the full class code:
https://github.com/zendframework/zf2/blob/master/library/Zend/Stdlib/ErrorHandler.php


A maybe better solution is that one from Monolog:

Link to the full class code:
https://github.com/Seldaek/monolog/blob/master/src/Monolog/ErrorHandler.php

It can also handle FATAL_ERRORS using the register_shutdown_function function. According to this class a FATAL_ERROR is one of the following array(E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR).

class ErrorHandler
{
    // [...]

    public function registerExceptionHandler($level = null, $callPrevious = true)
    {
        $prev = set_exception_handler(array($this, 'handleException'));
        $this->uncaughtExceptionLevel = $level;
        if ($callPrevious && $prev) {
            $this->previousExceptionHandler = $prev;
        }
    }

    public function registerErrorHandler(array $levelMap = array(), $callPrevious = true, $errorTypes = -1)
    {
        $prev = set_error_handler(array($this, 'handleError'), $errorTypes);
        $this->errorLevelMap = array_replace($this->defaultErrorLevelMap(), $levelMap);
        if ($callPrevious) {
            $this->previousErrorHandler = $prev ?: true;
        }
    }

    public function registerFatalHandler($level = null, $reservedMemorySize = 20)
    {
        register_shutdown_function(array($this, 'handleFatalError'));

        $this->reservedMemory = str_repeat(' ', 1024 * $reservedMemorySize);
        $this->fatalLevel = $level;
    }

    // [...]
}
algorhythm
  • 8,530
  • 3
  • 35
  • 47
10

I need to handle fatal errors for production to instead show a static styled 503 Service Unavailable HTML output. This is surely a reasonable approach to "catching fatal errors". This is what I've done:

I have a custom error handling function "error_handler" which will display my "503 service unavailable" HTML page on any E_ERROR, E_USER_ERROR, etc. This will now be called on the shutdown function, catching my fatal error,

function fatal_error_handler() {

    if (@is_array($e = @error_get_last())) {
        $code = isset($e['type']) ? $e['type'] : 0;
        $msg = isset($e['message']) ? $e['message'] : '';
        $file = isset($e['file']) ? $e['file'] : '';
        $line = isset($e['line']) ? $e['line'] : '';
        if ($code>0)
            error_handler($code, $msg, $file, $line);
    }
}
set_error_handler("error_handler");
register_shutdown_function('fatal_error_handler');

in my custom error_handler function, if the error is E_ERROR, E_USER_ERROR, etc. I also call @ob_end_clean(); to empty the buffer, thus removing PHP's "fatal error" message.

Take important note of the strict isset() checking and @ silencing functions since we don’t want our error_handler scripts to generate any errors.

In still agreeing with keparo, catching fatal errors does defeat the purpose of "FATAL error" so it's not really intended for you to do further processing. Do not run any mail() functions in this shutdown process as you will certainly back up the mail server or your inbox. Rather log these occurrences to file and schedule a cron job to find these error.log files and mail them to administrators.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Prof
  • 2,898
  • 1
  • 21
  • 38
9

Here is just a nice trick to get the current error_handler method =)

<?php
    register_shutdown_function('__fatalHandler');

    function __fatalHandler()
    {
        $error = error_get_last();

        // Check if it's a core/fatal error. Otherwise, it's a normal shutdown
        if($error !== NULL && $error['type'] === E_ERROR) {

            // It is a bit hackish, but the set_exception_handler
            // will return the old handler
            function fakeHandler() { }

            $handler = set_exception_handler('fakeHandler');
            restore_exception_handler();
            if($handler !== null) {
                call_user_func(
                    $handler,
                    new ErrorException(
                        $error['message'],
                        $error['type'],
                        0,
                        $error['file'],
                        $error['line']));
            }
            exit;
        }
    }
?>

Also I want to note that if you call

<?php
    ini_set('display_errors', false);
?>

PHP stops displaying the error. Otherwise, the error text will be send to the client prior to your error handler.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Sander Visser
  • 4,144
  • 1
  • 31
  • 42
7

PHP has catchable fatal errors. They are defined as E_RECOVERABLE_ERROR. The PHP manual describes an E_RECOVERABLE_ERROR as:

Catchable fatal error. It indicates that a probably dangerous error occured, but did not leave the Engine in an unstable state. If the error is not caught by a user defined handle (see also set_error_handler()), the application aborts as it was an E_ERROR.

You can "catch" these "fatal" errors by using set_error_handler() and checking for E_RECOVERABLE_ERROR. I find it useful to throw an Exception when this error is caught, then you can use try/catch.

This question and answer provides a useful example: How can I catch a "catchable fatal error" on PHP type hinting?

E_ERROR errors, however, can be handled, but not recovered from as the engine is in an unstable state.

Community
  • 1
  • 1
None
  • 5,491
  • 1
  • 40
  • 51
6

Since most answers here are unnecesarily verbose, here's my non-ugly version of the top voted answer:

function errorHandler($errno, $errstr, $errfile = '', $errline = 0, $errcontext = array()) {
    //Do stuff: mail, log, etc
}

function fatalHandler() {
    $error = error_get_last();
    if($error) errorHandler($error["type"], $error["message"], $error["file"], $error["line"]);
}

set_error_handler("errorHandler")
register_shutdown_function("fatalHandler");
Mahn
  • 16,261
  • 16
  • 62
  • 78
5

Not really. Fatal errors are called that, because they are fatal. You can't recover from them.

troelskn
  • 115,121
  • 27
  • 131
  • 155
3

There are certain circumstances in which even fatal errors should be caught (you might need to do some clean up before exiting gracefully and don’t just die..).

I have implemented a pre_system hook in my CodeIgniter applications so that I can get my fatal errors through emails, and this helped me finding bugs that were not reported (or were reported after they were fixed, as I already knew about them :)).

Sendemail checks if the error has already been reported so that it does not spam you with known errors multiple times.

class PHPFatalError {

    public function setHandler() {
        register_shutdown_function('handleShutdown');
    }
}

function handleShutdown() {
    if (($error = error_get_last())) {
        ob_start();
        echo "<pre>";
        var_dump($error);
        echo "</pre>";
        $message = ob_get_clean();
        sendEmail($message);
        ob_start();
        echo '{"status":"error","message":"Internal application error!"}';
        ob_flush();
        exit();
    }
}
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
tix3
  • 1,142
  • 8
  • 17
  • What is *"Sendemail"*? Do you mean *[Sendmail](https://en.wikipedia.org/wiki/Sendmail)* (respond by [editing your answer](https://stackoverflow.com/posts/13986716/edit), not here in comments)? – Peter Mortensen Apr 21 '20 at 01:52
3

I developed this function to make it possible to "sandbox" code that could cause a fatal error. Since exceptions thrown from the closure register_shutdown_function don't get emitted from the pre-fatal error call stack, I'm forced to exit after this function to provide a uniform way of using it.

function superTryCatchFinallyAndExit( Closure $try, Closure $catch = NULL, Closure $finally )
{
    $finished = FALSE;
    register_shutdown_function( function() use ( &$finished, $catch, $finally ) {
        if( ! $finished ) {
            $finished = TRUE;
            print "EXPLODE!".PHP_EOL;
            if( $catch ) {
                superTryCatchFinallyAndExit( function() use ( $catch ) {
                    $catch( new Exception( "Fatal Error!!!" ) );
                }, NULL, $finally );                
            } else {
                $finally();                
            }
        }
    } );
    try {
        $try();
    } catch( Exception $e ) {
        if( $catch ) {
            try {
                $catch( $e );
            } catch( Exception $e ) {}
        }
    }
    $finished = TRUE;
    $finally();
    exit();
}
Kendall Hopkins
  • 43,213
  • 17
  • 66
  • 89
0

As of PHP 7.4.13 my experience is that all possible errors and exceptions in a program can be caught with only two callback functions:

set_error_handler("ErrorCB");
set_exception_handler("ExceptCB");

ErrorCB simply reports its arguments in any way desired and calls Exit().

ExceptCB calls "get" methods on its exception argument and does some logic to determine where the file, line, and function are (ask me if you would like details), and reports the information in any way desired and returns.

The only need for try/catch is if you need to suppress errors for certain code, when @ or isset() isn't enough. Using try/catch for a "main function" without setting handlers fails, since it doesn't catch all errors.

If anyone finds code that generates an error that this approach doesn't catch, please let me know and I'll edit this answer. One error that this approach can't intercept is a single { character near the end of a PHP program; this generates a Parse error, which requires that you run your main PHP program via an Include file that contains the error handling.

I haven't found any need for register_shutdown_function().

Note that all I care about is reporting errors and then quitting the program; I don't need to recover from errors--that would be a much more difficult question indeed.

David Spector
  • 1,520
  • 15
  • 21
  • 1
    **require "NoExists.php";**: throws a warning followed by a fatal error, the warning is handled by a user error handler, but the fatal error is handled neither by a user error handler nor by a user exception handler nor by try/catch. This has been "fixed" since PHP 8. – Esteban Jan 01 '22 at 15:45