Categories
Uncategorized

Maybe returning errors in PHP

A common pattern I see in WordPress PHP code (and probably other PHP code) is a function which does some operation on data and returns a value. For example, a function which makes a database query and then returns the resulting row.

In order to make more robust code and prevent bugs, I’ve been systematically trying to use more return type declarations in my functions. This is a problem for things like the database function described above because a database query (and therefore the function itself) can sometimes fail. In those cases, the function might return the intended data, or it might return an instance of WP_Error instead. PHP does not currently (as of 7.1) support the idea of declaring return types when multiple types are possible.

Of course, such a function could simply throw an Exception if it fails, but this might be not as safe for critical code. An unhandled Exception, even a trivial one, could kill the whole program. Returning an error object instead could keep the program running (depending on how it is used). Even if we do throw an Exception, it’s not always clear when reading the function that it can throw.

So there are two problems now. First, since the function can return multiple things, we cannot specify any return type at all.

What I would need to solve that is the ability to specify multiple return types like phpdoc, but even if I could, it doesn’t guarantee that the user of this function will notice and handle potential errors.

This leaves us with the second problem, which is the same as if the function could throw an Exception; it can be easy for someone using it to overlook that the function can fail.

Is there a solution to both these problems? Maybe. (That’s a poor joke, as you’ll soon see.)

Let’s take the following function as an example. This function accepts an array of user data and returns the color property.

function getColorFromUser(array $user): string {
  return $user['color'];
}

It’s contrived, but it’s a reasonable simplification of the database query problem, since the array property might not exist, and therefore it can fail.

So now let’s modify the function to return an error if it fails.

function getColorFromUser(array $user) {
  if (! isset($user['color']) {
    return new WP_Error('NO_COLOR', 'Color does not exist');
  }
  return $user['color'];
}

As you can see, we’ve lost both the return type and also made it possible to introduce bugs in code that calls this function if they expect a string and instead get an object.

$myColor = getColorFromUser($user);
echo 'My favorite color is ' . $myColor; // This could cause a problem if $myColor is not a string

The standard way to handle this situation is similar to using a try/catch for an Exception; if we are aware that the function can return an error, we can check for one before continuing.

$myColor = getColorFromUser($user);
if (! is_wp_error($myColor)) {
  echo 'My favorite color is ' . $myColor;
}

It’s just that in my experience it’s common to miss the fact that a function can return an error. It’s even worse if a function once returned only a value but is subsequently changed to be able to return errors. Since the normal (successful) flow of the code will not change, no change is actually required anywhere the function is used. This makes type errors not only possible, but likely.

But what if we instead returned a wrapper object around the data? Let’s call it a Maybe. (See? There’s the joke.)

function getColorFromUser(array $user): Maybe {
  if (! isset($user['color']) {
    return Maybe::fromError(new WP_Error('NO_COLOR', 'Color does not exist'));
  }
  return Maybe::fromValue($user['color']);
}

Now we can have a return type, which can help prevent bugs inside the function. It’s not perfect as it doesn’t explain what types of values the object can contain, but it’s better than nothing. (To be fair, if we wished, we could create specific versions of Maybe for different values, like MaybeString, but I’m just going to keep it simple for now.)

Your first reaction might be that it also creates an inconvenience for the user of this function, because in order to extract the value of this data, someone has to do it explicitly.

But that small inconvenience means the developer must know about the possible error.

The example might then look something like the following. The condition required is similar to using is_wp_error(), but it’s more probable that the developer will remember to add the condition because they are forced to use getValue() on success.

$myColor = getColorFromUser($user);
if (! $myColor->isError()) {
  echo 'My favorite color is ' . $myColor->getValue();
}

This is not a perfect solution, and I’m certain that many developers will balk at having to add a pattern like this to their workflows when it’s not a standard part of the language. Personally I think it would be worth the discomfort to adopt this sort of construct when the benefit would be code that’s significantly less likely to have type errors. It probably depends on how careful you need your code to be. What do you think?

PS: It’s perhaps relevant to mention that there are functional programming concepts that are similar to this implementation, but which take it much further. My suggestion here is a very minimal interpretation that might be easier for some developers to grok. A more accurate name for this concept, matching existing patterns, is a Result.

Here’s how the Maybe/Result could be written:


<?php
class Maybe {
private $value;
private $error;
private function __construct($value, $error) {
$this->value = $value;
$this->error = $error;
}
public static function fromError($error): Maybe {
return new Maybe(null, $error);
}
public static function fromValue($value): Maybe {
return new Maybe($value, null);
}
public function isError(): bool {
return $this->error === null;
}
public function getError() {
return $this->error;
}
public function getValue() {
return $this->value;
}
}

view raw

Maybe.php

hosted with ❤ by GitHub

(Photo by Caleb Jones on Unsplash)

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s