Better PHP errors

20 Feb 2020 - Montpellier

Working one afternoon on a side project (of a side project) of a pet project, I encountered a fatal error, like many others:

[10:07:19] debug.INFO: Booting...
[10:07:19] debug.INFO: Loading driver...
PHP Fatal error:  Uncaught Facebook\WebDriver\Exception\WebDriverCurlException: Curl error thrown for http POST to /session with params: {"capabilities":{"firstMatch":[{"browserName":"firefox","platformName":"any","moz:firefoxOptions":{"args":["-headless"],"log":{"level":"info"},"prefs":{"devtools.console.stdout.content":true,"network.captive-portal-service.enabled":false,"browser.safebrowsing.enabled":false,"privacy.trackingprotection.enabled":false,"services.sync.prefs.sync.privacy.trackingprotection.enabled":false},"profile":"UEsDBAoAAAAAAOlQNVDf9RXUMgAAADIAAAAHAAAAdXNlci5qc3VzZXJfcHJlZigicmVhZGVyLnBhcnNlLW9uLWxvYWQuZW5hYmxlZCIsIGZhbHNlKTsKUEsBAj8DCgAAAAAA6VAAAcAAAAAAAAAAAAAALaBAAAAAHVzZXIuanNQSwUGAAAAAAEAAQA1AAAAVwAAAAAA"},"acceptInsecureCerts":true,"proxy":{"proxyType":"manual","httpProxy":"127.0.0.1:6789","sslProxy":"127.0.0.1:6789"}}]},"desiredCapabilities":{"browserName":"firefox","platform":"ANY","firefox_profile":"UEsDBAoAAAAAAOlQNVDf9RXUMgAAADIAAAAHAAAAdXNlci5qc3VzZXJfcHJlZigicmVhZGVyLnBhcnNlLW9uLW in /home/lithrel/Batcave/projects/brokenanchors/broken-anchors/vendor/facebook/webdriver/lib/Remote/HttpCommandExecutor.php on line 352

If you work in PHP, you're very much used to this way of reporting an error.

You quickly ignore the first words, jump to the namespace of the exception, assess if it's about your code or a vendor lib, go into a deep meditation while your eyes speed-read a blob of serialized json, come back to life for the file name and line number, and crawl into fetal position thinking about the past choices you made to end up in the darkest timeline.

Used to doesn't mean satisfied though, and a recent trip to Elm lang (as a side project of this side project) made me realize how brutal this was. You probably don't even see this error is telling me "You forgot to start selenium, dumbass.". I didn't either, for a very long minute. The next one will tell me "You forgot to start your proxy, dumbass.", and it will take me more than 10 minutes to realize that.
It's ok to be dumb. I'm pretty sure the engine and libraries could help me feel a bit less dumb.

The Elm's way

Elm has an amazing set of error messages from the compiler. The problem is clearly stated, most of the time a solution and/or a list of tips are given, and it's quite polite and pretty. There's a blog post by Evan Czaplicki explaining what it does and why it does it. The main points are:

Basically, thinking about an error message as a user experience.
And this makes an important point to me, the compiler not only rejects your work, it actually helps you grow.

Elm error message

A simple problem

Reading some of the PHP error messages, it is clear that you can gather some data from it and give an indication on how to solve the problem.
Let's take a basic syntax mistake that doesn't result in a parse error: using a string without quotes, dollar sign, or constant definition.
In PHP:

So if we write this simple code:

<?php
echo whatismypurpose;

We will get this error:

PHP Warning:  Use of undefined constant whatismypurpose - assumed 'whatismypurpose' (this will throw an Error in a future version of PHP) in /home/lithrel/Batcave/projects/php-errors/test1.php on line 2

PHP is assuming right away that we are trying to use a constant (it is, after all, the closest type according to the syntax we are using) and chooses to use it as a string 'whatami' (it won't do this in the next PHP version as it will throw an Error). From there, if you have just a little experience, you know you have to declare your constant before using it.

define('whatismypurpose', 'you pass butter');
echo whatismypurpose; // echoes 'you pass butter'

But was it what you were trying to do? The documentation states "By convention, constant identifiers are always uppercase", which my variable is clearly not.

Can we do it more like Elm?

Before tackling the problem of understanding your intention, we can try to make it look like Elm. In most Elm compiler errors, you get an error type, a file name, a bit of source code with the line in fault, a kind advice on how to solve your problem, and sometimes even links to the documentation. Let's mockup!

-- USE OF UNDEFINED CONSTANT ---------------------------------------- test1.php

This constant could not be found:

2|>    echo whatismypurpose;
            ^^^^^^^^^^^^^^^

So I used it as a string.

You should declare it like this:

    define('whatismypurpose', 'you pass butter');

Warning: This will throw an Error in a future version of PHP !

Read more about constants: 
    - https://www.php.net/manual/en/language.constants.php
    - https://www.php-fig.org/psr/psr-1/#4-class-constants-properties-and-methods

This looks a bit more friendly to me ! More verbose for sure, but less intimidating.

But we can do more:

Knowing all this, if the error message is about an uppercase string we can prioritize the hints given (let's take PHP_EOF as an undefined constant) :

-- USE OF UNDEFINED CONSTANT ---------------------------------------- test1.php

This constant could not be found:

2|>    echo PHP_EOF;
            ^^^^^^^

So I used it as a string.

You should declare it like this:

    define('PHP_EOF', 'foo');

There are a few possibilities:
    - You were looking for one of those constants
        PHP_EOL
        PHP_OS
        PHP_ZTS
    - You forgot simple quotes around your string `'PHP_EOF'`
    - You forgot a $ sign in front of your variable `$PHP_EOF`

This is only a Notice, but it will throw an Error in a future version of PHP !

Read more about constants: 
    - https://www.php.net/manual/en/language.constants.php
    - https://www.php-fig.org/psr/psr-1/#4-class-constants-properties-and-methods

And if it's a lowercase or mixed case string, we mostly reorder the possibilities :

-- USE OF UNDEFINED CONSTANT ---------------------------------------- test1.php

This constant could not be found:

2|>    echo whatismypurpose;
            ^^^^^^^^^^^^^^^

So I used it as a string.

There are a few possibilities:
   - You forgot simple quotes around your string `'whatismypurpose'`
   - You forgot a $ sign in front of your variable `$whatismypurpose`
   - You forgot to define your constant
       define('whatismypurpose', 'foo');
   - You were looking for one of those constants
       E_ERROR
       T_INT_CAST
       SORT_NATURAL

This is only a Notice, but it will throw an Error in a future version of PHP !

Read more about constants: 
   - Constants are always uppercase by convention
   - https://www.php.net/manual/en/language.constants.php
   - https://www.php-fig.org/psr/psr-1/#4-class-constants-properties-and-methods

Cute mockup, can we do it for real?

Sure we can. PHP provides a way to intercept most of the errors by defining your own error handler.
There's a lot to read at https://www.php.net/manual/en/book.errorfunc, and a good example to check is the way Monolog does it. Monolog is widely used in the PHP ecosystem to handle logging; it is also able to register itself as an error handler. Reading the code of src/Monolog/ErrorHandler.php, you can see how it catches errors and exceptions by declaring itself as a handler (set_error_handler, set_exception_handler), and fatal errors by registering a shutdown function (register_shutdown_function, error_get_last). Using this technique, we can catch an error, parse it and reformat it to offer a better experience.

And you say: it already exists

For any idea you have, a dozen people probably already posted about it on the internet. We will first look at PHP itself, and then at a few libs and frameworks.
Let's take an undefined function and look at PHP, and an extension Xdebug.

PHP raw

On the command line, PHP gives the type of the error, a message and a stacktrace.
On the web, PHP manages to put some bold text and <br/> if it knowns it's in html context (see main/main.c:1336 in php_error_cb() and the configuration of html_errors in php.ini) aaand that's about it.

CLI:
PHP CLI error

Web:
PHP web error

Xdebug enabled

Xdebug is an extension for PHP to assist with debugging and development.

Xdebug can add a stacktrace to Notices, and some color. It also gives memory usage and timestamp in the stacktrace.
On the web, it displays a very orange layout and a better presentation of variables in the context.

CLI:
XDebug CLI error

Web:
XDebug web error

Now, let's look for error handlers on packagist.org, and compare them to PHP. This will be a completely non-exhaustive list.

Symfony ErrorHandler component

Provides tools to manage errors and ease debugging PHP code.

Symfony has an interesting extensible take on this. For any exception, you can define an enhancer that will enrich the error message with any information you find relevant. For example, the UndefinedFunctionEnhancer helps you to find the same function in different namespaces. But it does not helps you with typos :/

The web presentation of the error is quite pretty, it has enough whitespace, colors and good layout. For whatever reason, the CLI side has colors but none of the layout effort given to the web page and no code excerpt.

Install :

composer require symfony/error-handler

Setup via static call :

Symfony\Component\ErrorHandler\ErrorHandler::register();

CLI:
Symfony web error

Web:
Symfony web error

whoops

PHP errors for cool kids

Pretty web layout, a lot of effort is made to display code and variables in the context of the error, the stack is readable, it can also link directly to your IDE, and even has google, duckduckgo and stackoverflow predefined links for a quick search ! On the CLI side it stays pretty close to the raw error, with a small template to make the stacktrace a bit more readable.

Install :

composer require filp/whoops

Setup with contextual handlers :

$whoops = new \Whoops\Run;
$whoops->pushHandler(new \Whoops\Handler\PrettyPageHandler);
$whoops->register();

CLI:
Whoops CLI error

Web:
Whoops web error

Collision

Collision is a beautiful error reporting tool for command-line applications

At last, some code excerpt on the command line :) And it uses JakubOnderka/PHP-Console-Highlighter, to display the proper code and the type of error. Nice !
It implements a SolutionsRepository contract, which allows it to display propositions of solutions as we'll see for Ignition.

Install :

composer require nunomaduro/collision --dev

Basic setup :

(new \NunoMaduro\Collision\Provider)->register();

CLI:
Collision error web

Ignition

Ignition: a beautiful error page for Laravel apps

Ignition is said to be specifically made for Laravel, so let's build up a basic Laravel app app and see this in action.

CLI:
Laravel cli error

Web:
Laravel web error

Ignition is leveraging its SolutionsProvider system gracefully. It will give hints if you keep default configuration options for example, perfect for beginners. It comes with a bunch of SolutionProviders and Solutions.
An interesting take in this system, is the RunnableSolution interface.

Ignition runnable solution An example of runnable solution from Ignition repo

On the web side, you can add a button with an action attached, action that will try to "fix" the problem detected.
I coded a similar system in my previous job (system which sadly never made it to production :/), on the customer side; if the error message and solution description are well written, it has multiple advantages: it will educate the customer and possibly lead to better autonomy, accelerate the execution of the solution, and relieve the customer support team of some repetitive tasks and discussions.
And I think it's a really good idea to give this to developers too.

Can we do better?

Formatting errors in a prettier way is already done by multiple libraries around, especially on the web side. I'm missing two things for all these:

Let's try this first, a simple lib handling some strings with parameters, an approximately agnostic way to describe layout and colors to be able to push it on CLI and web output, and some hardcoded errors for the mockup, here we are.

On the CLI side:
Hugger CLI error

and on the web side:
Hugger web error

This is a quick take, there could be code highlighting, a dark theme (obviously), a handler for links, etc. I don't need a stacktrace every time, but maybe adding an environment variable to display different informations, like Rust does with RUST_BACKTRACE=1, could be a solution for this; or displaying trace only for errors you don't have hints about.

Can we do even better?

Elm has this kind treatment for all possible errors, and it can do this because of language restrictions.
Now, can we treat ALL PHP errors this way? With a specific, contextual error message? For a language known for its extreme permissiveness and thus maybe unknown size of the domain of possible errors? According to StackOverflow, it's probably impossible to list all the errors.

List of all the possible PHP errors
Stack Overflow, crushing all your dreams and hopes since before you had those.

Still, I'd like to get a sense of what's there. How many, what type, how is it thrown anyway?
Errors are emitted by the PHP engine, which sources are readable on http://git.php.net/?p=php-src.git or https://github.com/php/php-src, we'll use github for now. The "undefined constant" error is thrown right there in Zend/zend_execute_API.c

zend_error(E_WARNING, "Use of undefined constant %s - assumed '%s' (this will throw an Error in a future version of PHP)", actual, actual);

by a function named zend_error(). By searching for all the zend_error calls, I might be able to list all the error messages that will appear in the lifetime of a PHP app. Let's look at its signature, we can find it in Zend/zend.h

ZEND_API ZEND_COLD void zend_error(int type, const char *format, ...) ZEND_ATTRIBUTE_FORMAT(printf, 2, 3);

This is when I realize I know nothing about C :|

No need to panic, it is fairly readable, and the internet is full of resources to decipher these strange hieroglyphs. Obviously this function takes an int as a type of error, a string to format the message, and a list of parameters to complete the error message. Yay! Searching for this should give me a lot of non-formatted error messages. But looking at this function headers, there's a bit more around it.

Here are defintions for what looks like different error functions:

ZEND_API ZEND_COLD ZEND_NORETURN void zend_error_noreturn(int type, const char *format, ...) ZEND_ATTRIBUTE_FORMAT(printf, 2, 3);
ZEND_API ZEND_COLD void zend_error_at(int type, const char *filename, uint32_t lineno, const char *format, ...) ZEND_ATTRIBUTE_FORMAT(printf, 4, 5);
ZEND_API ZEND_COLD ZEND_NORETURN void zend_error_at_noreturn(int type, const char *filename, uint32_t lineno, const char *format, ...) ZEND_ATTRIBUTE_FORMAT(printf, 4, 5);
ZEND_API ZEND_COLD void zend_throw_error(zend_class_entry *exception_ce, const char *format, ...) ZEND_ATTRIBUTE_FORMAT(printf, 2, 3);
ZEND_API ZEND_COLD void zend_type_error(const char *format, ...) ZEND_ATTRIBUTE_FORMAT(printf, 1, 2);
ZEND_API ZEND_COLD void zend_argument_count_error(const char *format, ...) ZEND_ATTRIBUTE_FORMAT(printf, 1, 2);
ZEND_API ZEND_COLD void zend_value_error(const char *format, ...) ZEND_ATTRIBUTE_FORMAT(printf, 1, 2);

Oh well, there's a char *format there too, I'm gonna have to parse those things if I want an 'exhaustive' list of errors.

Parse errors are a bit more complicated, I don't know if they might be enumerable. The language description itself seems to be in Zend/zend_language_parser.y, parse error is thrown in there, and that's all I have for now.

After a little time debugging, a very basic script (please don't judge me on this one) gives me 840 unique error messages.

Some PHP error messages

That looks like a lot of work to document all of those.
Now I'd like to be able to explore this data a bit, search in it, go back and forth between this and PHP sources. It's time to make an Elm frontend :D (granted I could just use my json export and jq, but where's the fun in that?)

Can we do evenen betterer?

Not having a stroke.

Back to Elm!
The frontend for exploration should basically read a json file and display every error message available. It should offer a search box that will filter on error message. To help with the exploration, and because the same error message can appear in different places in the code, it will also display, and filter on, the filename and line with a direct link to sources, type of function used to throw the error, and error level if relevant.
The result is functional (pun intended), it was satisfying to make, but most importantly satisfying to come back to and modify it. I still have some trouble understanding how to simply split or nest messages, but apart from that, a very smooth development experience.
I've used elm-ui out of curiosity after seeing a talk about it, the talk was really convincing, the usage was a bit more confusing.

A frontend to search for PHP errors
The result is available at randomdomainname.net/php-errors/

The first thing the exploration revealed: some error messages are not a string but a variable, that's what gives this empty error at the beginning of the list, so I might have to be a bit better at parsing, or write those by hand.

But wait, there's more

Can we do evenenen bettererer?

And then, it hits me. I now have

can I make a website that will show errors and display useful hints, even if out of context? It would require my error library to be able to give generic as well as contextual hints, but it would make for a very powerful tool for anybody desperately googling a PHP error (and not finding the right answer on StackOverflow).

After a bit of modifications, plugging the library in the parser script, adding hints in the generated json for each error message found, I get this:

A frontend with hints

which is very raw, but cute. I would need to parse colors and links. I could even put a textarea, and match a pasted error to the database to display contextual hints. But you know, code is the easy part, what needs to be done here is a lot of documentation, cleaverly layed out so it can be used contextually and agnosticly.

A kind of conclusion

A lot of effort in the community has been done in term of presentation (especially on the web side), but little in the explaination of things, especially native PHP errors, and probably for legitimate reasons; once you're familiar with the language and use a proper IDE, you shouldn't have too many problems solving these simple errors. You don't even see them anymore, just a few keywords and your mind automagically jumps to conclusion.

Still, I think there's a big part missing, the part I loved in Elm and Rust, the readable part, the explaination part, the guiding part. The PHP engine can be more than a punisher, it can be a teacher; it sits at the perfect spot between you and your running code, and it can do a lot more than just rejecting your work, it can show you the right way.

There are "naive" questions I don't ask myself anymore, like "If the compiler knows there's a semi-colon missing, why doesn't it add it itself?". I understand it usually is a bit more complicated than that, but I also think that not getting an explicit answer to that question is a mistake. A compiler can clearly say "I think there's a semi-colon missing there, but maybe you were trying to do something else, I can't fix this automatically." or even "I will try to fix it and execute again if you press f.". I think we can make for better error handlers and messages by asking the naive questions and answering them properly, instead of dismissing them as beginner problems, or as things everybody should endure "to learn".

The next best thing to do might be to create a PHP extension for this. It would have native access to the context of execution, which would make for easier deductions, hints and maybe even semi-automated fixes; it could be plugged on zend_throw_exception_hook() like xdebug does.
Either a PHP extension or a simple library, this project would still need a database of hints and solutions, and this work can be done without low level knowledge. This database could also be exported to symfony and laravel error systems, to enrich the php errors thrown at the developer.
With a common format to describe these hints and solutions, libraries developers could even write useful solutions for any framework out there to display and execute as it wishes.
In the end, I might realize I forgot to start my proxy a lot faster (feeling dumb only for a few seconds), in a less brutal way; or I could easily participate at documenting this specific error in this library.

Resources

P.S.

Seen a typo? Know a good generic hints & solutions data format? Something is horribly wrong and you want to extensively explain to me why? Hit me up on Mastodon or Twitter :).

P.P.S.

I'm actually looking for a job, ideally remote, you can find my full resume at https://florenthernandez.is/available!