When and What to Throw

Throw exceptions for exceptional situations. Sounds simple right? Standard advice? The problem is that it's hard to know what an exceptional situation is, and that definition changes from app to app and context to context.

I try to only throw exceptions when the routine (class, function, or method) can't continue its work. Say there's a method that downloads a CSV file from some SFTP server and reads it. If the download fails, the routine cannot complete -- there's nothing to read. That's an exception-worthy event.

On the flip side, something like a database access object (a DAO like a data mapper or repository) not finding an object its client requested is not an exception-worthy event. It might be exception worthy to the client, but that's not for the DAO to decide.

Those are two examples colored heavily with my opinions. They won't be the same in all cases. Use your judgement but think about the context in which the routine will be used as well as the domain.

What to Throw

There's two big groups of exceptional situations:

  1. Errors from the domain in which the code in question operates
  2. Anything else (database issues, misconfiguration, etc)

Your code should throw custom exceptions for the first and generic exceptions for the second.

Let's revisit the SFTP CSV file example above:

<?php
class CsvDownloader
{
    private $remoteName = 'some_filename_passed_to_constructor.csv';

    public function readFile()
    {
        $localName = tempnam();
        $downloaded = $this->sftpClient->download($this->remoteName, $localName);
        if (!$downloaded) {
            unlink($localName);
            throw new DownloadFailed('Could not download '.$this->remoteName, 0, $e);
        }

        // probably turn the file into arrays here or something...
    }
}

We care that the download failed -- that's an error in our domain. So we should wrap it up in a descriptive exception. Basically we want our domain exceptions to provide more information to the clients of our services. Even if those clients are simply catching and logging. throw new \Exception would give significantly less information.

What if our fictional sftpClient throws exceptions on its own? Should those be caught and wrapped?

I think it depends. Those types of errors -- errors in infastructure -- shouldn't be wrapped. Let them bubble up. An exception might be if the library in question didn't do a good job of specificity: maybe the library itself only throws generic Exception objects. If that's the case, wrapping the error in a custom exception might be a good idea.

Remember that DAO above? It provides another good example. Don't wrap exceptions that aren't specific to your domain. For instance, there's no reason to catch an exception that says your database credentials are bad. Let it throw and you'll get the information you need from your logs or error pages.

But the domain may call for uniqueness on a certain attribute. Catching exceptions that result from a unique constraint violation and rethrowing a domain exception that gives some appropriate context would be a good idea.

<?php

try {
    $this->db->insert(self::TABLE, [
        'title' => $title,
        'slug'  => $slug,
    ]);
} catch (UniqueConstraintViolation $e) {
    throw new DuplicatePostSlug(sprintf(
        'The slug "%s" already exists',
        $slug
    ), 0, $e);
}

The Domain Line

Finding the line between domain and not domain can be a bit tricky, but it really boils down to answer...

  • Do I care about this error?
  • Can I provide better context by using a custom exception?

Different subsystems within the same application might have different lines of what the domain is.

The SFTP client above, for instance, should care about SFTP -- that's its domain. So it's probably going to throw specific exceptiosn that give the client some information about what went wrong. The client code of that SFTP subsystem probably doesn't care about the specifics -- just that the sftp client failed and it can't continue its work.

Finally there's a balance of how much code you want to write.

<?php
class NotAValidInteger extends \Exception { }

function addIntegers($a, $b)
{
    if (!is_int($a)) {
        throw new NotAValidInteger('$a is not an integer');
    }
    if (!is_int($b)) {
        throw new NotAValidInteger('$b is not an integer');
    }

    return $a + $b;
}

The above is ridiculous. Just throw \InvalidArgumentException. Client code misusing a library or routine isn't a domain error. Use whatever generic exception your language provides (eg. some sort of RuntimeException).

<?php
function addIntegers($a, $b)
{
    if (!is_int($a) || !is_int($b)) {
        throw new \InvalidArgumentException(
            __FUNCTION__.' expects its arguments to be integer'
        );
    }

    return $a + $b;
}

#