PHP RFC: Exceptions in the engine
This RFC proposes to allow the use of exceptions in the engine and to allow the replacement of existing fatal or recoverable fatal errors with exceptions.
As an example of this change, consider the following code-snippet:
Currently the above code will throw a fatal error:
This RFC replaces the fatal error with an
EngineException. Unless the exception is caught this will still result in a fatal error:
Of course it is also possible to catch this exception:
Summary of current error model
PHP currently supports 16 different error types which are listed below, grouped by severity:
The first five errors are fatal, i.e. they will not invoke the error handler, abort execution in the current context and directly jump (bailout) to the shutdown procedure.
E_RECOVERABLE_ERROR error type behaves like a fatal error by default, but it will invoke the error handler, which can instruct the engine to ignore the error and continue execution in the context where the error was raised.
The remaining errors are all non-fatal, i.e. execution continues normally after they occur. The error handler is invoked for all error types apart from
Issues with fatal errors
Cannot be gracefully handled
The most obvious issue with fatal errors is that they immediately abort execution and as such cannot be gracefully recovered from. This behavior is very problematic in some situations.
As an example consider a server or daemon written in PHP. If a fatal error occurs during the handling of a request it will abort not only that individual request but kill the entire server/daemon. It would be much preferable to catch the fatal error and abort the request it originated from, but continue to handle other requests.
Another example is running tests in PHPUnit: If a test throws a fatal error this will abort the whole test-run. It would be more desirable to mark the individual test as failed, but continue running the rest of the testsuite.
Error handler is not called
Fatal errors do not invoke the error handler and as such it is hard to apply custom error handling procedures (for display, logging, mailing, …) to them. The only way to handle a fatal error is through a shutdown function:
This allows rudimentary handling of fatal errors, but the available information is very limited. In particular the shutdown function is not able to retrieve a stacktrace for the error (which is possible for other error types going through the error handler.)
Finally blocks will not be invoked
If a fatal error occurs
finally blocks will not be invoked:
doSomething() in the above example results in a fatal error the
finally block will not be run and the lock is not released.
Destructors are not called
When a fatal error occurs destructors are not invoked. This means that anything relying on the RAII (Resource Acquisition Is Initialization) will break. Using the lock example again:
doSomething() in the above example throws a fatal error the destructor of
LockManager is not called and as such the lock is not released.
finally blocks and destructors fail in face of fatal errors the only reasonably robust way of releasing critical resources is to use a global registry combined with a shutdown function.
Issues with recoverable fatal errors
After acknowledging that the use of fatal errors is problematic, one might suggest to convert fatal errors to recoverable fatal errors where possible. Sadly this also has several issues:
Execution is continued in same context
When a recoverable fatal error is dismissed by a custom error handler, execution is continued as if the error never happened. From a core developer perspective this means that a recoverable fatal error needs to be implemented in the same way as a warning is, with the assumption that the following code will still be run.
This makes it technically complicated to convert fatal errors into recoverable errors, because fatal errors are typically thrown in situation where continuing execution in the current codepath is not possible. For example the use of recoverable errors in argument sending would likely require manual stack and call slot cleanup as well as figuring out which code to run after the error.
Hard to catch
E_RECOVERABLE_ERROR is presented as a “Catchable fatal error” to the end user, the error is actually rather hard to catch. In particular the familiar
catch structure cannot be used and instead an error handler needs to be employed.
To catch a recoverable fatal error non-intrusively code along the following lines is necessary:
Exceptions provide an approach to error handling that does not suffer from the problems of fatal and recoverable fatal errors. In particular exceptions can be gracefully handled, they will invoke
finally blocks and destructors and are easily caught using
From an implementational point of view they also form a middle ground between fatal errors (abort execution) and recoverable fatal errors (continue in the same codepath). Exceptions typically leave the current codepath right away and make use of automatic cleanup mechanisms (e.g. there is no need to manually clean up the stack). In order to throw an exception from the VM you usually only need to free the opcode operands and invoke
Exceptions have the additional advantage of providing a stack trace.
This proposal consists of two parts: Several general policy changes, as well as particular technical changes.
The RFC proposes the following policy changes:
- It is now allowed to use exceptions in the engine.
- Exceptions originating from the engine should be of type
EngineException, but can also use a different type in justifiable exceptional cases (e.g.
- Existing errors of type
E_RECOVERABLE_ERRORcan be converted to exceptions.
- It is discouraged to introduce new errors of type
E_RECOVERABLE_ERROR. Within limits of technical feasibility the use of exceptions is preferred.
A new class
EngineException extends Exception is introduced. It exhibits the same behavior as the ordinary
Exception class, but skips one stack frame (this is necessary to produce correct stack traces when throwing directly from the VM).
Internally the following APIs are added:
Exceptions sometimes need to be thrown before all opcode operands have been fetched. In this case the operands still need to freed, but the ordinary
FREE_OP* VM pseudo-macros cannot be used. To solve this several VM-macros/functions are introduced:
Furthermore the patch accompanying this RFC contains initial work for replacing existing
E_RECOVERABLE_ERROR errors with exceptions. In particular it removes all uses of
Currently it is possible to silently ignore recoverable fatal errors with a custom error handler. By replacing them with exceptions this capability is removed, thus breaking compatibility.
I have never seen this possibility used in practice outside some weird hacks (which use ignored recoverable type constraint errors to implement scalar typehints). In most cases custom error handlers throw an
ErrorException, i.e. they emulate the proposed behavior with a different exception type.
If these concerns are considered significant this RFC might be restricted to
E_ERROR conversions only. Personally I doubt that this will result in any significant breakage, but I can't claim extensive knowledge in this area.
catch-all blocks in existing code
Exception it will be caught by catch-blocks of type
catch (Exception). This may cause existing code to inadvertently catch engine exceptions.
If this is considered to be an issue one possible solution is to introduce a
Exception extends BaseException, which will be the new base of the exception hierarchy. Only exceptions that are considered unlikely to require catching in anything save top-level handlers will directly inherit from this type. Both Python (
BaseException) and Java (
Throwable) make use of this concept.
EngineException could then extend
BaseException rather than
Cluttered error messages
Going back to the code-sample from the introduction, this is the fatal error that is currently thrown:
With this RFC the error changes into an uncaught exception:
The uncaught exception message provides more information, e.g. it includes a stack-trace which is helpful when debugging the error, but it is also rather cluttered. Especially when working on the terminal the long
Fatal error: Uncaught exception 'EngineException' with message prefix pushes the actual message so far to the right that it has to wrap. Things also become quite confusing when the exception message contains quotes itself.
I think it would be nice to make those messages a bit cleaner (for all exceptions). The following adjustment is simple to do and seems more readable to me:
Additional improvement (like removing the
Fatal error: prefix and the duplicate file/line information) would require special handling in
Not all errors converted
The Zend Engine currently (master on 2013-12-10) contains the following number of fatal-y errors:
The count was obtained using
git grep “error[^(]*(E_ERROR_TYPE” Zend | wc -l and as such may not be totally accurate, but should be a good approximation.
The patch attached to the RFC currently (as of 2013-10-24) removes 70
E_ERRORs and 11
E_RECOVERABLE_ERRORs. While I hope to port more errors to exceptions before the patch is merged, the process is rather time consuming and I will not be able to convert all errors. (Note: The number of occurrences in the source code says rather little about what percentage of “actually thrown” errors this constitutes.)
Some errors are easy to change to exceptions, others are more complicated. Some are impossible, like the memory limit or execution time limit errors. The
E_CORE_ERROR type can't be converted to use exceptions because it occurs during startup (at least if used correctly).
E_PARSE) currently also can't be converted to exceptions, due to concerns regarding global state modifications.
Converting most existing errors will take some time and in the meantime we'll be in a situation where some part of the errors were converted to exceptions but another part stays fatal. From a user perspective it may not be immediately clear when one is used over the other.
While this may be slightly inconvenient, I strongly think that it's better to start fixing this gradually, rather than waiting until the time (that will never come) where we can fix everything at once.
E_ERROR portion of this proposal does not break backwards compatibility: All code that was previously working, will continue to work. The change only relaxes error conditions, which is generally not regarded as breaking BC.
E_RECOVERABLE_ERROR part of the proposal may introduce a minor BC break, because it will no longer allow to silently ignore recoverable errors with a custom error handler. As this point is somewhat controversial I'll have a separate voting option for this.
A preliminary patch for this RFC is available at https://github.com/nikic/php-src/compare/engineExceptions.
The patch introduces basic infrastructure for this change and removes all
E_ERROR uses from
zend_vm_def.h (as well as some other errors).
This is a yes/no vote with the additional option of implementing the proposal, but without changing
E_RECOVERABLE_ERRORs (as that part of the proposal may have BC issues).
As this is a language-related change, the vote requires a two-third majority. The 3-way vote will be interpreted as follows: If 2/3 of the total votes are for “Yes”, the proposal is implemented fully. If 2/3 of the votes are for “Yes” or “Yes, without E_RECOVERABLE_ERROR changes” then the proposal is implemented without the
E_RECOVERABLE_ERROR changes. Otherwise, the proposal is not implemented.
If you are in favor of this proposal in general, but not for PHP 5.6, use the “No” voting option.
Vote started on 2013-12-07 and ended on 2013-12-14.