On Exceptions

Recently there was a discussion on the php-usergroup slack about Exceptions and whether they should have DateTime information available or not.And that brought me to write down my personal ideas on what I think an Exception is.

First of all, an Exception is not for the user but for the code. Have a look at this piece of code:

class MyArray
{
    private $entries = [];

    public function getEntry(int $key)
    {
        return $this->entries[$key];
    }
}

What happens when I do this:

    $array = new MyArray();
    $array->getEntry(1);

Exactly: I get a Notice like this:

Notice: Undefined offset: 1 in php shell code on line 7

Call Stack:
   10.0997     355216   1. {main}() php shell code:0
   10.0997     355216   2. MyArray->getEntry() php shell code:1

So my code within the getEntry-method can’t work properly because someone might feed information in that can’t work. How would one react to such a condition?

Breaking the flow

class MyArray
{
    private $entries = [];

    public function getEntry(int $key)
    {
        if (! array_key_exists($key, $this->entries) {
            die ("key can't be found");
        }
        return $this->entries[$key];
    }
}

Breaking the flow by crashing the application would be an option but in my eyes not really a good one.

Logging an error

class MyArray
{
    private $entries = [];

    public function getEntry(int $key)
    {
        if (! array_key_exists($key, $this->entries) {
            error_log("key can't be found");
        }
        return $this->entries[$key];
    }
}

Justs logging the issue will not keep me from receiving the notice and the problem is not really fixed… So that’s also not really an option.

Return NULL

class MyArray
{
    private $entries = [];

    public function getEntry(int $key)
    {
        if (! array_key_exists($key, $this->entries) {
            return NULL;
        }
        return $this->entries[$key];
    }
}

Returning NULL (or FALSE) is an option that looks feasible but it doesn’t really feel right. The method returns a value that is not really stored within the array and the application isn’t notified that the requested key is not really valid. And the method itself doesn’t know what might be the appropriate return value for the calling code to work correctly.

Throw an Exception

So to me it looks like requesting a key that is not available results in a condition that is exceptional for this method. So it calls for throwing an Exception. In this case perhaps an instance of an InvalidArgumentException as the method receives an argument that is not valid in it’s current context.

class MyArray
{
    private $entries = [];

    public function getEntry(int $key)
    {
        if (! array_key_exists($key, $this->entries) {
            throw new InvalidArgumentException("key can't be found");
        }
        return $this->entries[$key];
    }
}

This clearly shows to the calling code that something was wrong and it gives the calling code also the possibility to react appropriately. So the calling code would look like this:

    $array = new MyArray();
    try {
        $value = $array->getEntry(1);
    } catch (InvalidArgumentException $) {
        $value = 'Sorry, but nothing could be retrieved';
    }

So an exception can tell the calling code that something exceptional has happened and we as developers have a possibility to tell the application how to react to that. That’s called fault tolerance.

So when we – for example – have a request-object implementing \Psr\Http\Message\ ServerRequestInterface the query parameters might be held within such an Array-Object. The implementation of getAttribute($name, $default = null) might then look something like this:

    public function getAttribute($name, $default = null)
    {
        try {
            /** @var $this->attributes MyArray */
            return $this->attributes->getEntry($name);
        } catch (InvalidArgumentException $e) {
            return $default;
        }
    }

So within this code you can set a default that should be returned as it knows about that but the MyArray-Class still does not need to know what to use as default.

So whatever we as developers decide to do with the Exception depends on us. In the above example we handle the exception gracefully so that the application can continue to run. So there’s no need for any “human interaction”. The only information to handle the exception within the code is the type of the exception, the message and an optional exception-number. Usually the file, line and backtrace-info isn’t really necessary for that.

Definitely not necessary would be a DateTime information for an Exception. Why? Well, the Exception is thrown to tell the application that something weird happened right now and that it should handle that. And that usually happens within milliseconds.

But sometimes we as developers want to be informed about exceptions that are not handled. And in that case it might be of interest to know when the Exception was thrown. But how can we achieve that? Well, when an exception is not handled at all the application will crash. So we need to catch all exceptions at a certain point to be able to keep the application running and to be informed about the issue. That means at one point in the application we need to implement an ExceptionHandler. In PHP we can use set_exception_handler to handle all Exceptions that where not caught anywhere within the code. So we can use that to inform someone about what happened like this:

set_exception_handler(function(Throwable $e)) {
    error_log($e->getMessage());
});

And here we might want to add the information about the date and time. But it will not be the date and time the exception was thrown, but when it was logged!

set_exception_handler(function(Throwable $e)) {
    error_log((new DateTime())->format('Y-m-d H:i:s') . ': ' . $e->getMessage());
});

That might not be the “correct” time the exception was thrown, but the difference would be merely milliseconds and what would you actually gain from knowing the exact millisecond the Exception was thrown at? Especially as adding the datetime-information to the Exception also takes some CPU-cycles and might be irrelevant in most of the cases due to it being handled internally within the code.

So in my opinion the datetime-information is not really necessary within an Exception. Even the stack trace would be debatable but as that is not at all findable in hindsight I’D say it is good to have that within the Exception.

But in general I see an Exception as something to create fault tolerant code and only in very rare cases something we should need to log. And only when logging the DateTime information is necessary. So let’s add that information at logging and not at throwing the Exception.

Or do you see that differently?

2 thoughts on “On Exceptions

  1. I agree with your conclusion, but I think your example is flawed in how exceptions are used.

    When I look at the code using the MyArray::getEntry() method which throws the exception it looks like the exception is used for control flow. The MyArray class should rather have a method hasEntry() which would allow to check for the presence of an entry with the key first. The code using the MyArray class shouldn’t catch an exception at all (at least in the example, this might be different depending on the circumstances of the actual code).

    Having said that, the MyArray::getEntry() method should still throw an exception if the key doesn’t exist. However, I think that it shouldn’t be an InvalidArgumentException. It should be such an exception only when the key value can’t be used as a key at all. Rather I’d throw something like a KeyMissingException (name subject to better ideas). Oh, and I wouldn’t suffix it with Exception, but we had a similar discussion in the past where I failed to convince you. 😉

    1. I generalized the example in several ways to try to make the point more clear.

      You are right in that the MyArray-Class should have a hasEntry-method so that you can check beforehand whether the key exists. Nevertheless you should never rely on that in the actual getEntry-method. There still might be invalid input due to someone (deliberately or not) not checking beforehand whether the key exists. So in my eyes the getEntry -method still should throw an exception. And in the calling code you then still need to decide how to handle that possibility. Whether that’s an InvalidArgumentException (thats one that is readily available in every PHP installation) or perhaps a more appropriate ArrayKeyOutOfBoundsException or an ArrayKeyDoesNotExistException is a completely separate story (and might be a topic for a further blog-post). And whether that Exception has the suffix Exception or not is a completely separate story 😉

Comments are closed.