I made this statement on twitter about PHP libraries throwing exceptions that I want to expand here.
I’d say there are two broad categories of exceptions that a library might throw.
Domain Exceptions
These are specific to the library’s domain. For instance, if I’m working with AdWords I know that AdWords account IDs take the format 111-222-3333
. That might be represented with a value object that protects that invariant and throws a very specific exception when something is wrong with the incoming data. If I’m writing a library for AdWords, this is a domain-specific error. Like the value object that contains that domain rule, something that breaks it deserves its own named, specific exception.
“Generic” Exceptions
A library might include a set of things like its own InvalidArgumentException
that extends the exception of the same name from the root namespace. Sometimes this happens, sometimes not. Sometimes it makes more sense to always use domain exceptions, sometimes not. There’s a balance in here somewhere.
Why Not Throw Root Namespace Exceptions?
I view throwing library specific exceptions (via a marker interface, described below) as a form of Inversion of Control (Ioc).
When a library throws exceptions from the root namespace it robs its clients of the ability to handle specific library errors if desired. By using custom types a library gives control of the library back to its client. The client may decide to handle those library exceptions specifically or ignore them.
Can all of these library exceptions be handled? That’s not a question that the library should get a say in.
Practical Examples
One of the cool things about PHP (and Java) is that the exception name in a catch block need not actually be a Throwable
implementation. A great way to offer library exceptions that work with the usual exception suspects is with a marker interface.
<?php namespace Some\Library; interface LibraryException { // don't really need anything here // can extends \Throwable if desired }
Remember that AdWords domain exception example?
<?php namespace Some\Library; // extend whatever makes sense here final class InvalidAdWordsId extends \DomainException implements LibraryException { // noop } final class AdWordsClientId { private $id; public function __construct(string $id) { if (!preg_match('/^\d{3}-\d{3}-\d{4}$/', $id)) { throw new InvalidAdWordsId(/* explanation maybe */); } $this->id = $id; } public function __toString() : string { return $this->id; } }
Or here’s a generic InvalidArgumentException
.
<?php namespace Some\Library; // I know this is ugly on multiple lines // but it looks better on the page class InvalidArgumentException extends \InvalidArgumentException implements LibraryException { }
This gives the library client the option of globally catching an exception from a library (useful at times) or dealing with specific errors. Often times the specific error handling is useful to give application users feedback when they messed up inputs or something happened during runtime that they can correct themselves.
try { $id = new AdWordsClientId('badFormat'); } catch (LibraryException $e) { // generic catch } try { $id = new AdWordsClientId('badFormat'); } catch (InvalidAdWordsId $e) { // specific catch }
There are two great things about this approach:
- It can be added to an existing library that throws root namespace exceptions. Throw an
InvalidArgumentException
? A library still can, but it will throw a subclass of it. - It’s easy to buy into this strategy without going to the crazy domain exception area. What to throw “generic” exceptions. Do it, but make them the library’s own generic exceptions.
Corollary: Don’t Let Exceptions From Another Layer Escape
Say a library is an API client that uses Guzzle to do its work. Those guzzle exceptions shouldn’t leak out from the library.
Ideally they will be wrapped with domain specific exceptions. A 400 response from an API means something specific in the domain in which that API exists. A good library will indicate that.
Conclusion
It’s important to note that this applies to reusable, library code only. This may make sense in an application as well, but it may not.
As always: do what works.