Catch a fatal exception and continue - php

I know, that by its very definition, a fatal exception is supposed to kill the execution, and should not be suppressed, but here's the issue.
I'm running a script that scrapes, parses and stores in a DB about 10,000 pages. This takes a couple of hours, and in rare cases (1 in 1000) a page fails parsing and throws a fatal exception.
Currently, I'm doing this:
for ($i=0;$i<$count;$i++)
{
$classObject = $classObjects[$i];
echo $i . " : " . memory_get_usage(true) . "\n";
$classDOM = $scraper->scrapeClassInfo($classObject,$termMap,$subjectMap);
$class = $parser->parseClassInfo($classDOM);
$dbmanager->storeClassInfo($class);
unset($classDOM,$class,$classObject);
}
Can I do something like
for ($i=0;$i<$count;$i++)
{
$classObject = $classObjects[$i];
echo $i . " : " . memory_get_usage(true) . "\n";
try
{
$classDOM = $scraper->scrapeClassInfo($classObject,$termMap,$subjectMap);
$class = $parser->parseClassInfo($classDOM);
$dbmanager->storeClassInfo($class);
unset($classDOM,$class,$classObject);
}
catch (Exception $e)
{
//log the error here
continue;
}
}
The code above doesn't work for fatal exceptions.
Would it be possible to do something like this:
If I moved the main loop into a method, and then call the method from register_shutdown_function ?
Like this:
function do($start)
{
for($i=$start;$i<$count;$i++)
{
//do stuff here
}
}
register_shutdown_function('shutdown');
function shutdown()
{
do();
}
This is the message that is output when execution stops:
Fatal error: Call to a member function find() on a non-object in ...
I expect this above message when a page isn't parse-able by the method I am using. I'm fine with just skipping that page and moving on to the next iteration of the loop.

Fatal errors are fatal and terminate execution. There is no way around this if a fatal error occurs. However, your error:
Fatal error: Call to a member function find() on a non-object in ...
is entirely preventable. Just add a check to make sure you have an instance of the correct object, and if not, handle the error:
if ($foo instanceof SomeObject) {
$foo->find(...);
} else {
// something went wrong
}

First, there is a distinct difference between exceptions and errors. What you encountered is an error, not an exception. Based on your message and the code you posted the problem is with something you haven't put into your question. What variable are you trying to call find() on? Well, that variable isn't an object. There is no way to trap fatal errors and ignore it, you must go find where you are calling find() on a non-object and fix it.

Seems to me like the only possible way to "catch" a faltal error is with the registering a shutdown function. Remember to add all (or maybe groups of) queries into transactions and maybe roll them back if something fails, just to ensure consistency.

I have had a similar problem to this, and I found that using a is_object() call before the find() call allows you to avoid the fatal error.

Related

How can I catch an exception and continue despite an error

I'd like to able to catch an exception and continue with the execution of other subsequent functions (and possibly log an error in the catch section). In the code sample below, there are instances where $html->find doesn't find the element and returns error exception undefined offset. In such cases, the entire script fails. I don't want to specifically test for this error but rather any error that may occur within the code block in the try section.
public function parsePage1($provider)
{
$path = $this->getFile($provider);
$link = $this->links[$provider];
if (file_exists($path)) {
$string = file_get_contents($path);
$html = \HTMLDomParser::str_get_html($string);
$wrapper = $html->find('.classToLookFor')[0];
unset($string);
}
}
try {
$this->parsePage1('nameOfProvider');
} catch(Exception $e) {
// continue...
}
try {
$this->parsePage2('nameOfProvider');
} catch(Exception $e) {
// continue...
}
No, there is no way to make the code within the try block continue past an exception. An exception terminates the function just like a return would; there is no way to restore the state of the function afterwards.
Instead, avoid triggering the error in the first place:
$wrappers = $html->find('.classToLookFor'); # <-- no [0]!
if (count($wrappers)) {
$wrapper = $wrappers[0];
...
}
Just to be clear, the 'error' in this case was a notice. If your errorlevel does not include notices, which is typically the case in production, your code will continue past that point.
With that said, Notices and warnings are intended for developers to add checks for expected input, as in duskwuff's example.
Unfortunatley, duskwuff's answer is problematic with the most recent versions of php at 7.2+. This is because count() expects either an array or an object that implements countable.
With the newest version you will get a Warning:
Warning: count(): Parameter must be an array or an object that implements Countable in
You will be back where you were before using count() only. A simple fix for that is to add a check for is_array.
$wrappers = $html->find('.classToLookFor'); # <-- no [0]!
if (is_array($wrappers) && count($wrappers)) {
$wrapper = $wrappers[0];
...
}
I also want to point out, that per my original comment, the whole purpose of exception catching is to protect against program termination errors.
This was not a good example of the types of errors where you should apply try-catch, but to be clear, your original code does continue... just not within the try section of the code, but after the catch()
This simulation of your original problem illustrates that:
<?php
function findit($foo) {
return $foo[0];
}
try {
findit('');
} catch(Exception $e) {
var_dump($e);
}
echo 'Hey look we continued';
Output will be something like:
Notice: Uninitialized string offset: 0 in ... on line 4
Hey look we continued
I feel this needs to be added as a response because people in the future are going to probably find this question, which really has nothing much to do with try-catch handling, and really has to do with code that expects to work with an array, but might not get one.

Is this an uncatchable fatal error?

I'm writing an application, where I thought all the errors were being handled, including fatal ones.
But now I found one error that results in a white screen, and the error only shows up in the webserver log.
$nonExistentVar + 1; // Notice error, gets caught and pretty error is displayed
$existentVar->nonExistentMethod(); // Fatal error, gets caught and pretty error is displayed
$nonExistentVar->nonExistentMethod(); // White screen, error can be seen in nginx.error.log
Is the last error uncatchable? Or what could the problem be?
I'm using Silex, not sure if that matters.
The way I understand it, exceptions can be caught but fatal errors cannot. I am curious to know how you are 'catching' the fatal error in example #2?
Why not use a php is_a() test to see if $nonExistentVar is of the correct class before attempting to call the method? Or possibly in conjunction with method_exists() if you still don't know if a class has a given method available.
Try putting only the last line:
$nonExistentVar->nonExistentMethod();
That works for me, as Symfony\Component\Debug\ExceptionHandler sends the response immediately upon encountering the first Error:
public function handle(\Exception $exception)
{
if (class_exists('Symfony\Component\HttpFoundation\Response')) {
$this->createResponse($exception)->send();
} else {
$this->sendPhpResponse($exception);
}
}

Unable to catch a 'catchable fatal error' from QueryPath

I have a script that scrapes some old HTML. It does about 1000 pages a day, and every so often it chokes for some reason and throws up the following error:
PHP Catchable fatal error: Argument 1 passed to DOMXPath::__construct() must be an instance of DOMDocument, null given, called in /var/scraper/autotrader/inc/QueryPath/QueryPath/CSS/DOMTraverser.php on line 417 and defined in /var/scraper/autotrader/inc/QueryPath/QueryPath/CSS/DOMTraverser.php on line 467
At first I thought it was the error was generated when htmlqp($html) was called, but I have wrapped it in a try{} statement and it didnt catch anything:
UPDATE:
I've found the offending line of code by using # to see when the script would terminate without error. It's this line:
try {
$items = $html->find('.searchResultHeader')->find('.vehTitle'); //this one
} catch (Exception $e) {
var_dump(get_class($e));
echo 'big dump'.$e->getTraceAsString();
}
When it bombs out, it doesn't even echo 'big dump', so it really doesn't seem to be catching it.
I'm wondering if this is maybe a fault with QueryPath's error handling rather than my own?
This:
$html->find('.searchResultHeader')->find('.vehTitle');
is the same as this:
$html->find('.searchResultHeader .vehTitle');
But without the risk of calling null->find();
If you really want to do it in 2 steps, use an if, not a try:
if($el = $html->find('.searchResultHeader')) $items = $el->find('.vehTitle');
Or maybe a ternary:
$items = ($el = $html->find('.searchResultHeader')) ? $el->find('.vehTitle') : null;
It is not catching because a standard try catch block will not catch errors of this type. In order to catch a 'Catchable' fatal error a Set Error Handler for the E_RECOVERABLE_ERROR is needed.
See Also: How can I catch a “catchable fatal error” on PHP type hinting?.

fatal error call to undefined method MDB2_error::disconnect()

I am running a php script in command line that connects to a oracle and mssql to fetch some data and write to a file. Actually it was a cron on linux machine which needed to be transfered to windows 2008.
The command is throwing the error:
fatal error call to undefined method MDB2_error::disconnect() in
path\to\script.php in line63
The code around line 63 are:
$db_clw = MDB2::factory($config->database->CLW->dsn);
if (PEAR::isError($db_clw)) {
$db_clw->disconnect();
$db_banner->disconnect();
die($db->getMessage());
}
any idea?
You are calling the disconnect method on a MDB2 error object. That method does not have a disconnect method.
$db_clw = MDB2::factory($config->database->CLW->dsn);
if (PEAR::isError($db_clw)) {
$db_clw->disconnect();
// ^ method does not exist
$db_banner->disconnect();
die($db->getMessage());
}
Since you call die immediately, there is probably no need to use disconnect at all, but if $db_clw is MDB2_Error, it has no method disconnect, so you should not attempt to call it. The attempt to call it will only occur if there is an error.
When it throws an error here
$db_clw->disconnect();
You already know that $db_clw is not a MDB2 Driver, but rather an error. As such, it doesn't have a disconnect method, so that line should be deleted.
You might want to surround your other disconnect statement there with a try-catch, such as:
$db_clw = MDB2::factory($config->database->CLW->dsn);
if (PEAR::isError($db_clw)) {
//We now know $db_clw is an error, don't attempt to disconnect.
try {
$db_banner->disconnect();
} catch (Exception e) {} //ignore it.
//die($db->getMessage()); Not sure if this is what you want, I'd think
die($db_clw->getMessage())
}
ignoring any problems with disconnecting, so that the statement die($db->getMessage()); is reached, which will help you determine why $db_clw = MDB2::factory($config->database->CLW->dsn); is failing.
Just noticed, and updated the code above to change the last statement to die($db_clw->getMessage());, which seems, probably, to be what your looking for there.

Handle fatal errors in PHP using register_shutdown_function()

According to the comment on this answer it is possible to catch Fatal Errors through a shutdown function which cannot be caught using set_error_handler().
However, I couldn't find out how to determine if the shutdown has occured due to a fatal error or due to the script reaching its end.
Additionally, the debug backtrace functions seem to be defunct in the shutdown function, making it pretty worthless for logging the stack trace where the Fatal Error occured.
So my question is: what's the best way to react on Fatal Errors (especially undefined function calls) while keeping the ability to create a proper backtrace?
This works for me:
function shutdown() {
$error = error_get_last();
if ($error['type'] === E_ERROR) {
// fatal error has occured
}
}
register_shutdown_function('shutdown');
spl_autoload_register('foo');
// throws a LogicException which is not caught, so triggers a E_ERROR
However, you probably know it already, but just to make sure: you can't recover from a E_ERROR in any way.
As for the backtrace, you can't... :( In most cases of a fatal error, especially Undefined function errors, you don't really need it. Pinpointing the file/line where it occured is enough. The backtrace is irrelevant in that case.
One way to distinguish between fatal error and proper application shutdown with the register_shutdown_function is to define a constant as the last line of your program, and then check if the constant is defined:
function fatal_error() {
if ( ! defined(PROGRAM_EXECUTION_SUCCESSFUL)) {
// fatal error has occurred
}
}
register_shutdown_function('fatal_error');
define('PROGRAM_EXECUTION_SUCCESSFUL', true);
If the program reaches the end, it could not have encountered a fatal error, so we know not to run the function if the constant is defined.
error_get_last() is an array with all the information regarding the fatal error that you should need to debug, though it will not have a backtrace, as has been mentioned.
Generally, if your php program has encountered a fatal error (as opposed to an exception), you want the program to blow up so you can find and fix the problem. I've found register_shutdown_function useful for production environments where you want error reporting off, but want some way to log the error in the background so that you can respond to it. You could also use the function to direct the user to a friendly html page in the event of such an error so that you don't just serve up a blank page.
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) {
//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 wan't 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

Categories