- URL: https://www.laruence.com/en/2010/08/03/1697.html
- Please include attribution when republishing.
What is the mechanism behind PHP's exceptions?
What is the ZEND_HANDLE_EXCEPTION at the end of every independently executable op array in PHP for?
Let's start with a question. Last week, blue5tar asked: "For the code below, onError clearly ran, but onException never did. Why?"
<?php
function onError($errCode, $errMesg, $errFile, $errLine) {
echo "Error Occurred\n";
throw new Exception($errMesg);
}
function onException($e) {
echo $e->getMessage();
}
set_error_handler("onError");
set_exception_handler("onException");
/* I would never name a file after myself, so this file doesn't exist */
require("laruence.php");
The result:
Error Occurred PHP Fatal error: main(): Failed opening required 'laruence.php'
First, we need to know that when Require includes a file it cannot find, it throws two errors, one after the other:
1. WARNING : thrown while PHP tries to open the file. 2. E_COMPILE_ERROR : thrown after the file-opening function returns failure.
And as we know, set_error_handler cannot catch E_COMPILE_ERROR:
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.
So inside onError, only that first WARNING is caught. But then, why was the exception thrown inside onError not caught by the default exception_handler?
That brings us to PHP's exception mechanism.
Anyone familiar with opcodes (Understanding PHP Internals: Opcodes) knows that before PHP 5.3, the last opcode of every independently runnable op array (file, function, method) was ZEND_HANDLE_EXCEPTION. So what is that opcode for?
It turns out that when an exception is thrown in PHP, execution jumps to the last line of every op array to run this ZEND_HANDLE_EXCEPTION. The pseudo code looks like this:
void on_throw_exception(zval *exception TSRMLS_DC) {
1. Check whether an exception has already been thrown
2. Record the exception
3. Record the index of the next op line to execute
4. Next op line index to execute = the last one of the current op array
}
Yeah, it's just like rewriting the ip register: rewrite the index of the next op line to execute and you change the flow of the program, which lands you in the ZEND_HANDLE_EXCEPTION handling logic.
And in ZEND_HANDLE_EXCEPTION, PHP decides whether the exception is inside a try catch:
If yes set the next op line to execute to the first catch's op line, and keep executing. If no destroy some variables and opline that are no longer needed, then end execution immediately.
Some of you may ask: "Then when does the default exception handler set by set_exception_handler (user_exception_handler) take effect?"
Well, only after execution finishes and the execution LOOP exits does PHP check whether there is a default exception handler, and call it if there is:
//execute
zend_execute(EG(active_op_array) TSRMLS_CC);
if (EG(exception)) {
if (EG(user_exception_handler)) {
call the user defined default exception handler
} else {
uncaught exception
}
} else {
no exception
}
destroy_op_array(EG(active_op_array) TSRMLS_CC);
efree(EG(active_op_array));

Note: one thing in the diagram is not rigorous — when deciding whether it is the last catch block, (is_a) is checked at the same time, and only if it matches does execution enter the last catch block.
And when PHP hits a Fatal Error, it calls zend_bailout directly, and zend_bailout makes the program flow skip the code above entirely — you can think of it as an immediate exit (longjmp). That is what leaves user_exception_handler with no chance to run.
With all that, the "why" of the question at the beginning of the article should be pretty clear, shouldn't it?
Finally, about ZEND_HANDLE_EXCEPTION, some of you may wonder: if that's the case, why does every independently executable op array have this ZEND_HANDLE_EXCEPTION at the end? At the very least, if a function never throws, this opcode is obviously unnecessary, isn't it? Heh, you're smart — starting with PHP 5.3, PHP has already been adjusted the way you thought: a ZEND_HANDLE_EXCEPTION opline is only generated dynamically at the moment of a throw.
PHP5 changelog:
Changed exception handling. Now each op_array doesn't contain ZEND_HANDLE_EXCEPTION opcode in the end. (Dmitry)
Be First to Comment