I've got an elaborate script that relies on exception throwing/catching to deal with errors. Yesterday, I tried to run it on a newly allocated virtual server and it ran fine as it was looping, waiting for a batch of data to arrive. When the batch of data became available, it forked off processes to handle the data and each process died quietly except those child processes had exited with an error condition. The code looks something like this:

$res = pcntl_wait($status); // wait until a child ends up
MTLog::getInstance()->debug2('$res = pcntl_wait($status); called with $res = '.$res);

if ( $res>0 ) {
  // this is fine!
  MTLog::getInstance()->debug('Finishing child with pid '.$res);
} else {
  // this is bad and was the result i was getting
  $error_msg = 'Outch1, this should not happen. Verify your implementation.';
  $error_msg .= "\n" . $this->getWaitStatusInfo($status) . "\n";
  $error_msg .= "Outch1 recorded in file " . __FILE__ . " at line " . __LINE__ . "\npcntl_wait returned " . print_r($res, TRUE) . "\n" . $this->reportErrors();
  MTLog::getInstance()->error($error_msg);
  // added by jta to optionally send a notification
  $this->tryNotification($error_msg);

  $this->run = false;
  break;
}

As you can see I've take pains to write a log when this happens and send myself an email notification. The problem is that the error message I received was not helpful in finding the problem:

2013-06-13 00:09:03 [18260] ERROR : Outch1, this should not happen. Verify your implementation.STATUS: 65280
pcntl_wifexited: TRUE
pcntl_wexitstatus: 255
pcntl_wifsignaled: FALSE

Outch1 recorded in file /root/ImageDaemon/class.MTDaemon.php at line 404
pcntl_wait returned -1
error_get_last did not return any errors

There was no remaining evidence of the fatal errors that ended my child processes available at the point where I realize there has been an error. After some time-consuming digging, I realized that this server's PHP installation doesn't have the curl extension. The result was that the code tried to call a function which didn't exist resulting in a fatal E_ERROR being thrown and the script dying/exiting with an error condition. The "PHP Fatal error: Call to undefined function curl_init() in /foo/bar/file.php on line XX" error message that would have been most helpful in finding the problem was lost forever for a variety of reasons:
1) php's error_log setting is empty meaning that error messages go to STDERR for scripts invoked via CLI
2) The process that encountered the fatal error was forked off from a process that was itself forked off from the actual cli script I had evoked so whatever got sent to STDERR never actually bubbled up to a terminal window or output file so that I could see it
3) my code relies heavily on exception handling but so far I haven't found a way to catch an E_ERROR. According to the docs on [man]set_error_handler[/man], you can't handle these with your own error function:

The following error types cannot be handled with a user defined function: E_ERROR, E_PARSE, E_CORE_ERROR, E_CORE_WARNING, E_COMPILE_ERROR, E_COMPILE_WARNING, and most of E_STRICT raised in the file where set_error_handler() is called. 

I nevertheless tried to make a function which turned errors into thrown exceptions but it doesn't help when you try to call a function that doesn't exist:

<?php
class foo {

public function __construct() {
	set_error_handler(array($this, "error_handler"));

	try {
		echo "here i am\n";

		this_function_does_not_exist();
		trigger_error("a user error", E_USER_ERROR);

		echo "made it to the end\n";
	} catch (Exception $e) {
		die("exception caught:" . $e->getMessage() . "\n");
	}
	echo "function foo completed\n";
}

public static function error_handler($errno, $errstr, $errfile=NULL, $errline=NULL, $errcontext=NULL) {
	$msg = " handled by my error handler in file " . $errfile . " at line " . $errline . "\n===\n" . $errstr . "\n===\n";
	switch ($errno) {
		case E_ERROR:
			$msg = "E_ERROR: " . $msg;
			die("fatal error\n");
			break;
		case E_USER_ERROR:
			$msg = "E_USER_ERROR: " . $msg;
			break;
		case E_WARNING:
			$msg = "E_WARNING: " . $msg;
			break;
		case E_NOTICE:
			$msg = "E_NOTICE: " . $msg;
			break;
		case E_STRICT:
			$msg = "E_STRICT: " . $msg;
			break;
		default:
			$msg = "unrecognized errno ($errno): " . $msg;
	}

	throw new Exception("Exception " . $msg);
}
} // class foo
$v = new foo();
?>

Is there some way to turn a fatal E_ERROR into an exception instead?

    1) php's error_log setting is empty meaning that error messages go to STDERR for scripts invoked via CLI

    Worst-case: maybe you could just stick an ini_set() into the offending cli script(s) to set the error_log setting to log (even email) errors as/where desired?

      Thanks for the suggestion. I've tested this and it does work:

      ini_set("error_log", "/path/to/error.log");
      

      However, it's not especially helpful to have it written to a separate file (inconvenient) and without any contextual information about which process was running, etc. e.g.:

      [14-Jun-2013 11:49:56] PHP Fatal error:  Call to undefined function this_function_does_not_exist() in /var/www/chump.php on line 5

      I'm really hoping I might be able to catch the fatal error in my PHP code and deal with it there rather than having to snoop around and cross-correlate things. I'll be running a lot of these servers so the more convenient it is the better.

        Yes, fatal errors can be converted to exceptions.
        There's no point in throwing them, however.

        set_error_handler(
          function( $code,$message,$file,$line ){
            print 'useful error message/handling goes here';
            $severity = 0; // useless param used by ErrorException
            throw new ErrorException( $message,$code,$severity,$file,$line );  // uncatchable;
                                                                               // fatal errors always terminate
            return true; 
          }    // with fatal errors, script dies here no matter what
        );
        
        try{
          require 'not_a_file';
        }
        catch( Exception $e ){
          print $e->getMessage();  // will never be seen
        }
        
        print ' ...hello!'; // will never be seen

        Anything you want to do has to be done inside the error handler function.

        My solution was to put the error handler function inside my error handling class, and then tie it into all the same logic that my uncaught exceptions used. I created an ErrorException, but didn't throw it: I stored it as a property of the error handler class, and skipped right to the "uncaught/fatal exception" method.

          Unless I'm mistaken, you cannot intercept E_ERROR at all. Try it.

            sneakyimp;11029753 wrote:

            Unless I'm mistaken, you cannot intercept E_ERROR at all. Try it.

            did: the snippet I posted outputs

            useful error message/handling goes here

            followed by (because I have [FONT=monospace]display_errors[/FONT] on; not from catching the exception - as I said, you can't catch the exception):

            [B]Fatal error[/B]: Unknown: Failed opening required 'not_a_file' ...

            You can intercept the fatal error; you just can't recover from it afterwards.

              instead of a require statement for non-existent file, try calling a nonexistent function (which is the problem that I had when curl extension was missing). this script has no output at all on my machine:

              set_error_handler(
                function( $code,$message,$file,$line ){
                  print "useful error message/handling goes here";
                  $severity = 0; // useless param used by ErrorException
                  throw new ErrorException( $message,$code,$severity,$file,$line );  // uncatchable;
                                                                                     // fatal errors always terminate
                  return true; 
                }    // with fatal errors, script dies here no matter what
              );
              
              try{
                this_is_not_a_function();
              } catch( Exception $e ) {
                print $e->getMessage();  // will never be seen
              }
              
              print " ...hello!"; // will never be seen 
              

                interesting... you're right.

                php.net/set_error_handler wrote:

                The following error types cannot be handled with a user defined function: E_ERROR, E_PARSE, E_CORE_ERROR, E_CORE_WARNING, E_COMPILE_ERROR, E_COMPILE_WARNING, and most of E_STRICT raised in the file where set_error_handler() is called.

                I wonder why it works for require (which php.net claims issues an E_COMPILE_ERROR on failure)...?

                ### edit ###
                it would appear that, contrary to the documentation, [man]require[/man] issues E_WARNING, not E_COMPILE_ERROR.

                set_error_handler(
                  function( $code,$message,$file,$line ){
                    switch( $code ){
                      case E_ERROR: die( 'error' );
                      case E_COMPILE_ERROR: die( 'compile' );
                      case E_CORE_ERROR: die( 'core' );
                      case E_PARSE: die( 'parse' );
                      case E_RECOVERABLE_ERROR: die( 'recoverable' );
                      case E_USER_ERROR: die( 'user' );
                      case E_WARNING: die( 'warning' );
                      default: die( $code );
                    }
                  }
                );
                
                require 'not-a-file';
                warning

                ### edit#2 ###
                see my post below

                  traq;11029761 wrote:

                  it would appear that, contrary to the documentation, [man]require[/man] issues E_WARNING, not E_COMPILE_ERROR.

                  lame! maybe i'll email the php dev list (or the doc list)?

                  I'm starting to think that a shutdown function might be something to try...check last_error.

                    the plot thickens...

                    Now you've got me confused. This is the method I register via [man]set_error_handler[/man] in my current project:

                        public static function error( $code,$message,$file,$line,$context ){
                            // using  $severity to flag this as a user-triggered error vs. php error
                            $severity = in_array( $code,[E_USER_DEPRECATED,E_USER_ERROR,E_USER_NOTICE,E_USER_WARNING] )?
                                self::user_error:
                                self::php_error
                            ;
                            // store error info
                            self::$_error_ = new \ErrorException( $message,$code,$severity,$file,$line );
                            // script execution resumes once trivial errors are logged
                            if( in_array( $code,[E_DEPRECATED,E_NOTICE,E_STRICT,E_USER_DEPRECATED,E_USER_NOTICE] ) ){
                                return self::log();
                            }
                            // fatal php errors must be dealt with now
                            if( $code === E_ERROR || $code === E_USER_ERROR ){
                                self::log();
                                self::abort( 500 );
                            }
                            // other php errors are thrown back to be caught by application;
                            if( $severity === self::php_error ){
                                throw self::$_error_;
                                return true;
                            }
                            // ...while user errors go directly to the exception handler.
                            self::exception();
                        }

                    ...and it handles fatal errors as expected. For example, I can do something like [font=monospace]not_a_function()[/font] and my ::log function (because I'm using debug mode) prints out something like:

                    ATapp\DEBUG info
                    Warning: DEBUG mode is for development only. ATapp\DEBUG should never be truthy for publicly accessible sites. Use the error log instead.
                    
                    Array
                    (
                    [COLOR="#B22222"][B]    [error_code] => [1] E_ERROR[/B][/COLOR]
                        [file] => /usr/AT/ATapp/MOD/LIB/CTRL/APIS/FrontControllerAbstract.php
                        [line] => 100
                        [message] => Call to undefined function ATapp\MOD\LIB\CTRL\APIS\not_a_function()
                        [trace] => Array
                            (
                                [0] => Array
                                    (
                                        [file] => /usr/AT/ATapp/MOD/LIB/CTRL/ErrorHandler.php
                                        [line] => 297
                                        [function] => error
                                        [class] => ATapp\MOD\LIB\CTRL\ErrorHandler
                                        [type] => ::
                                        [args] => Array
                                            (
                                                [0] => 1
                                                [1] => Call to undefined function ATapp\MOD\LIB\CTRL\APIS\not_a_function()
                                                [2] => /usr/AT/ATapp/MOD/LIB/CTRL/APIS/FrontControllerAbstract.php
                                                [3] => 100
                                                [4] => 
                                            )
                    
                                )
                    
                            [1] => Array
                                (
                                    [function] => ATapp\MOD\LIB\CTRL\{closure}
                                    [class] => ATapp\MOD\LIB\CTRL\ErrorHandler
                                    [type] => ::
                                    [args] => Array
                                        (
                                        )
                    
                                )
                    
                        )
                    
                    )

                    ...but I'm getting the same problem you describe when using only the snippet. stumped!

                      The documentation seems to be suggesting something about which file we set this value in, etc....kinda weirdly suggestive:

                      The following error types cannot be handled with a user defined function: E_ERROR, E_PARSE, E_CORE_ERROR, E_CORE_WARNING, E_COMPILE_ERROR, E_COMPILE_WARNING, and most of E_STRICT raised in the file where set_error_handler() is called.

                      If errors occur before the script is executed (e.g. on file uploads) the custom error handler cannot be called since it is not registered at that time.

                      I have no idea what that italicized (my italics) is supposed to mean.

                        For the immediate problem, you could check for configuration dependencies before you execute your code...

                        if(!extension_loaded('curl'))
                            {
                            // either work without curl or just die()
                            }

                        In other words, it seems like a better way to check if a function exists might be to actually do so ([man]extension_loaded/man, [man]function_exists/man, etc.) rather than blindly attempting to call it and hoping you can clean up after the mess left behind if it wasn't actually there. My mindset is always in embedded software all day, so I liken this to checking if a pointer is NULL before attempting any dereferencing rather than just blindly using the pointer and letting the CPU exception handler deal with the resulting chaos.

                          I'm not sure how your code is structured and would be interested in knowing where/how you assign the error handler.

                          Also, the comments on [man]set_error_handler[/man] are pretty interesting and show most people suggesting the use of a shutdown function.

                            sneakyimp;11029767 wrote:

                            The documentation seems to be suggesting something about which file we set this value in, etc....kinda weirdly suggestive:

                            The following error types cannot be handled with a user defined function: E_ERROR, E_PARSE, E_CORE_ERROR, E_CORE_WARNING, E_COMPILE_ERROR, E_COMPILE_WARNING, and most of E_STRICT raised in the file where set_error_handler() is called.

                            If errors occur before the script is executed (e.g. on file uploads) the custom error handler cannot be called since it is not registered at that time.

                            I have no idea what that italicized (my italics) is supposed to mean.

                            nor do I; since the [noparse]

                            [/noparse] tags italicize everything 🙂.

                            sneakyimp wrote:

                            I'm not sure how your code is structured and would be interested in knowing where/how you assign the error handler.

                            Ha, good question! I'd forgotten about this (in my error handling startup method):

                            register_shutdown_function(
                                        function(){
                                            if(
                                                ($e = error_get_last())
                                                && (
                                                    $e['type'] === E_ERROR
                                                    || $e['type'] === E_USER_ERROR
                                                )
                                            ){
                                                list( $code,$message,$file,$line ) = array_values( $e );
                                                self::error( $code,$message,$file,$line,null );
                                            }
                                        }
                                    );

                            ...completely forgot about that. SO, you're on the right track!

                            sneakyimp wrote:

                            Also, the comments on set_error_handler are pretty interesting and show most people suggesting the use of a shutdown function.

                            indeed, they are.

                            bradgrafelman;11029769 wrote:

                            For the immediate problem, you could check for configuration dependencies before you execute your code...

                            Agreed - startup check.

                              bradgrafelman;11029769 wrote:

                              For the immediate problem, you could check for configuration dependencies before you execute your code...

                              Yes I have already done this.

                                traq, I like your syntax and was wondering what would happen if you were to register that shutdown function inside a class somewhere. interestingly, you use an anonymous function in your assignment which persists after the instance of the class is gone:

                                class foo {
                                	public function __construct() {
                                		register_shutdown_function(
                                			function(){
                                				if($e = error_get_last()) {
                                					echo "HERE IS AN ERROR\n" . print_r($e, TRUE) . "\n";
                                				}
                                			}
                                		);
                                	} // __construct()
                                } // class foo
                                
                                
                                $f = new foo();
                                unset $f;
                                this_is_not_a_function();
                                
                                

                                Output:

                                PHP Fatal error:  Call to undefined function this_is_not_a_function() in /home/jaith/biz/erep/ImageDaemon/foo.php on line 19
                                HERE IS AN ERROR
                                Array
                                (
                                    [type] => 1
                                    [message] => Call to undefined function this_is_not_a_function()
                                    [file] => /home/jaith/biz/erep/ImageDaemon/foo.php
                                    [line] => 19
                                )
                                

                                I like it.

                                  I like it too.
                                  Every day, there's something in 5.4 that I discover and can't imagine how I ever lived without. : )

                                  That call is actually inside a static function (the whole class is static), so it's never instantiated at all. You can see it here if you're interested (the [font=monospace]register_shutdown_function[/font] call is inside the [font=monospace]start()[/font] method, line 295).

                                    traq wrote:

                                    Every day, there's something in 5.4 that I discover and can't imagine how I ever lived without. : )

                                    There are some nifty things coming in 5.5 (like this).

                                      I've been reading about generators - I get the basic concept, but I haven't fully wrapped my head around it. PHP development seems to have really spiked though, hasn't it? Great time to be alive 🙂

                                        Write a Reply...