Anyone who has seen me talk in the #wordpress-dev IRC room will know that I’m
not a huge fan of WP_Error
. However, for some insane reason, some people are.
I figured it’s probably time to explain why WP_Error
sucks, and what we can do
about it.
Conception of WP_Error
Back in the days of WordPress 2.0, errors were handled by returning false from
WordPress functions, or occasionally error strings. For 2.1, it was decided
to change this to returning an error object instead. This error object gave the
ability to indicate an error had occurred, but still include information with
the error that could be used programmatically, or as a user-friendly message.
Given the context of its conception, WP_Error
was a great idea; it gave the
easy ability to pass data regarding errors around while still noting that it
was an error, rather than actual data.
State of the Error
Currently, most WordPress functions return a WP_Error
object if something goes
wrong. Based on what I wrote just before, this might seem like an awesome idea.
However, imagine what happens if I have a helper function:
/**
* Retrieve and decode JSON data from a URL
*/
function rmccue_my_http_helper($url) {
$response = wp_remote_get($url);
if (is_wp_error($response)) {
return $response;
}
return json_decode($response['body']);
}
This might seem fine, but note that we have to handle WP_Error differently here.
Errors give no useful information to this function, so we could just return
false. However, this deprives the caller function of the ability to find out
about the error.
For an example of when this becomes unwieldy, let’s look at what happens when
the above function gets used:
/**
* Get current message from API
*/
function rmccue_get_api_msg() {
$data = rmccue_my_http_helper('http://api.example.com/');
if (is_wp_error($data)) {
return $data;
}
return $data['apidata']['messages']['latest'];
}
/**
* Output result to header
*/
function rmccue_output_message() {
$message = rmccue_get_api_msg();
if (is_wp_error($message)) {
echo $message->get_error_message();
}
else {
echo $message;
}
}
Note that we now have three places where we’re checking if we got an error
back, but only one place where that check is actually useful (i.e. when we
output it).
Even worse than this is when developers forget to check for errors (I’ll admit,
I’ve been guilty of this many times). Suddenly, they’re trying to use a
WP_Error
object as an array or an integer, and PHP will fail, or worse, give
garbage output.
I Take Exception to That!
As anyone who has worked with WordPress knows, WordPress supported PHP 4 for a
long time, even after many other projects had switched. The advantage of this
was supporting significantly more hosts, with most PHP 5-only features either
not being needed or being easy to reimplement.
One of the new features added to PHP in PHP 5 was exception handling. For those
who aren’t aware of it, exception handling is a way to indicate an error and
have it handled at an appropriate place without having to check values
constantly. This might sound familiar to you: isn’t this what WP_Error
was
intended to solve?
The answer is yes, but not quite. WP_Error
is essentially the poor man’s
exception. Unlike WP_Error
, the basic idea of exceptions is that you only
worry about errors where they actually matter and lower-level functions can
forget about needing them. Exceptions continue up the call stack until they’ve
been caught, when they can then be handled as necessary.
This might seem a bit confusing, so here’s what our previous example would look
like if we used exceptions instead (assuming wp_remote_get()
threw a
WP_Exception
exception):
/**
* Retrieve and decode JSON data from a URL
*/
function rmccue_my_http_helper($url) {
$response = wp_remote_get($url);
return json_decode($response['body']);
}
/**
* Get current message from API
*/
function rmccue_get_api_msg() {
$data = rmccue_my_http_helper('http://api.example.com/');
return $data['apidata']['messages']['latest'];
}
/**
* Output result to header
*/
function rmccue_output_message() {
try {
$message = rmccue_get_api_msg();
echo $message;
}
catch (WP_Exception $exception) {
echo $exception->get_error_message();
}
}
See the difference? Instead of having to check at every level for exceptions, we
can now just let the exception pass up to somewhere that matters.
How does this work? In this case: if wp_remote_get()
throws an exception, this
is passed up to rmccue_my_http_helper()
. There’s no try ... catch
in this
function, so we continue up the callstack, through rmccue_get_api_msg()
until
we hit the try ... catch
in rmccue_output_message()
. Here, we catch the
exception and handle it as appropriate.
Exceptions also provide valuable context for developers. Rather than having to
check all the places where the error could have occurred, every exception
includes a traceback; that is, the entire callstack up until when the exception
was thrown. This gives you an easy way to see where an error occurred and makes
debugging much easier.
How We Can Start Using Exceptions Now
Although WordPress doesn’t use exceptions internally, you can already start
using them. For example, Renku uses them internally to save on a lot of
code.
The basic concept of using exceptions in your code is simple: whenever you get
a WP_Error
object, convert it to an exception. In our above example, this
would mean handling it in rmccue_my_http_helper()
and
rmccue_output_message()
, but we’d no longer have to handle it inbetween.
Here’s what the above would look like:
/**
* Retrieve and decode JSON data from a URL
*/
function rmccue_my_http_helper($url) {
$response = wp_remote_get($url);
if (is_wp_error($response)) {
throw new Exception($response->get_error_message(), $response->get_error_code())
}
return json_decode($response['body']);
}
Here, we convert the WP_Error
to an exception as soon as possible, allowing us
to skip most of the extra handling in our code.
What About Core?
Unfortunately, exceptions don’t appear to be getting into core any time soon.
Some of the core developers are very against exceptions (for reasons I can’t
completely comprehend). One of the arguments made against using exceptions in
core was the possibility of confusing theme developers. I’d actually make the
counter-argument that WP_Error
is more confusing to theme developers. Having
to check at every possible stage if a result is_wp_error()
is much more
confusing and is not something that theme developers are necessarily going to
remember.
Another of the issues I’ve heard raised is that of the fatal nature of
exceptions. Any exceptions that haven’t been caught by the time they get to the
top-level are handled by a default exception handler, or failing that, cause a
fatal error. The solution for WordPress is easy: firstly, add a default
exception handler that uses wp_die()
, much like the existing handling for
fatal errors; secondly, add a try ... catch
inside
do_action()
/apply_filters()
. Most plugins run the majority of their code
inside actions/filters, so this would ensure that any exceptions would only
cause that specific callback to fail. This would keep WordPress running with
minimal interruptions to the existing workflow.
The only issue that I can see is one of backwards compatibility. The best way to
deal with that would be to announce that exceptions will be used in two releases
time (for example), and to encourage developers to switch to it. WP_Error
could immediately be changed to extend WP_Exception
(which would in turn
extend Exception
). This would give the ability for proactive plugin developers
to switch easily. For example, our HTTP helper function:
/**
* Retrieve and decode JSON data from a URL
*/
function rmccue_my_http_helper($url) {
$response = wp_remote_get($url);
if (is_wp_error($response)) {
if (class_exists('WP_Exception') && $response instanceof WP_Exception) {
throw $response;
}
else {
throw new Exception($response->get_error_message(), $response->get_error_code())
}
}
return json_decode($response['body']);
}
This would give complete forward and backward compatibility for these plugins
and enable a smooth transition to exceptions.
So, what are you waiting for? Get out there and use exceptions!