For Nerds: reliably capturing and displaying warnings, notices, and errors in PHP

May 31, 2023

As much as we love working with PHP, we must admit that the debugging experience out of the box isn't great. The messages can be cryptic, poorly formatted, and sometimes invisible!

Due to the inline output of these error messages, you can miss all kinds of useful information if the output happens inside a HTML tag, for example a <SELECT> HTML element may contain an invisible error if the code broke mid-HTML output. The programmer will have to hunt through 'view source' to find the error.

Computer programming can be plenty frustrating on it's own! After 13 years of putting up with this slowing down my process, i decided to do something about it.

Our solution & our why

When I designed the Zerolith framework, i knew it was possible to improve on this behavior. Zerolith is also extremely unconventional and there wasn't a lot of 'prior work' to riff off of. So i built a debugger into it from the start.

I wanted the debugger to show all possible messages and warnings/errors/notices in a pop-up window after script execution is terminated so that i never lost them in HTML. That way, i would be assured that i knew what was going wrong immediately.

I had also planned for a bespoke unit testing system and automatic logging of any issues in production apps, so accurately collecting this information and being able to send it somewhere was crucial.

So how's it work?

NOTE: All of this code is in PHP 7.4, however will work up to 8.2.x as of writing.

Did you know that you can execute additional code after the end of a script, or after a fatal error? we take great advantage of that here!

In Zerolith, we use a globally available buffer to capture warnings/notices. When execution is finished, we call a termination handler that outputs this data that was collected in a popup window. This ensures that our output never gets lost. We also catch fatal errors and present them nicely to the developer.

Here's how these hooks work:

register_shutdown_function is called on a few fatal PHP errors, as well as during a graceful end of execution, so we pipe this into a termination routine function.
set_exception_handler catches catastrophic errors and returns an exception object to that function, so we also pipe that into our termination routine.
set_error_handler can be used to report/catch anything from an 'info' message from PHP to a full blown error. It also reports details on these events. We're gonna use this to produce a log of these events to display it later, and pass actual errors to the function specified in set_exception_handler so that we don't change PHP's error handling logic ( trust me, you want to avoid doing that! ).

Here's how we set those error handlers. zl::terminate() is our termination handler.

By outputting our captured error message last, after PHP is "done", we can escape any HTML code we are currently stuck in. We'll also add absolute positioning to the output element to give it a consistent space to pop up in also.

With this trick, we can even escape a tricky situation like this, where the error would normally be hidden ( the jeah() function doesn't exist ).

Our termination handler also needs some logic to handle all the different kinds of input it's getting. There's some code gymnastics needed to accurately determine whether the shutdown was due to an error or not.

This function is pretty long, so here's the important part that determines if there was a major error in the code or not.

Here's the warning trap. We handle events from set_error_handler here, and in the case of serious errors, we return false which tells PHP to decide to refer the event to set_exception_handler.

Because PHP's exception objects from set_exception_handler resemble backtraces, in order to get nicely formatted output, there are also numerous functions in our framework to handle that. We'll leave that out for brevity's sake.

Example debugger ouput

In the case of no fault in the code, the debugger will show in the bottom left on the screen minimized.

In the case that you didn't have an error in the script, the debugger window will pop open if there are warnings etc. This guarantees you will see all of them - instead of a decent probability of these being hidden in HTML code also.

If you had an error early in the execution, and the function that starts to draw the page can't execute, this debugger will not attempt to draw itself; instead it will output the faulting error directly to screen with bare-bones formatting.

If zerolith's page start function has been called, we can draw the debugger. In this case, we can use our backtrace formatting function ( and a lot of other code ) to help us print out a useful trace:

The end result: if anything is iffy with the code, we guarantee there is a visible ( and helpful ) error message, instead of the usual hit/miss indication from PHP.

How well does it work?

This solution will work for ~95% of all possible conditions. A good IDE like PHPstorm will show a majority of the remaining 5% by highlighting very obviously broken code red.

Trappable ( will show error )Non-trappable ( PHP produces a 500 error )
Warnings
Notices
Execution errors
End of execution
Parse Errors ( outside of root script )
Compile Errors ( outside of root script )
Fatal errors ( outside of root script )
Parse Errors ( in root script )
Compile Errors ( in root script )
Fatal errors ( in root script )

Other possible party tricks of this bug trapper/displayer:

  • Write detailed error information to the database in the event of an error on the production server so you can track serious errors in your application.
  • Send an email to a developer when something goes very wrong in a critical piece of code.
  • Automatically inform a custom unit testing system that there was an error or warning in some of the code.
  • Determine with 95% accuracy whether it is suitable to show the end-user a notification that the page had an error, and do so in a resilient way.

I hope this has been helpful!
-David

Leave a Reply

Your email address will not be published. Required fields are marked *

>Contact Us
>Privacy Statement
>Terms of Service