Exceptions will always be a constant topic when it comes to Object-Oriented Programming, and today, let's find out how to create them like true software artisans!
1. Introduction
When I started learning programming, one of the topics that always scared me was "errors" or anything related to it. However, as I studied more frequently, I realized that errors and/or Exceptions are much more friends than enemies. Of course, to achieve that, you need to understand how to use them in an interesting way for your project.
In my case, I used to use the throw new Error()
snippet for literally anything, easily getting lost in the codebase due to a generic Exception scattered among many others. In the beginning, it wasn't a problem since I didn't work in a team with observability, so it was all good.
But over time, as I joined more awesome companies, I encountered excellent implementations of Exceptions, especially using the Factory Pattern
. This method amazed me in how things can be simple and elegant
, even when dealing with errors.
Today, I'll show you how to develop a taste for writing elegant Exceptions and avoid cluttering your code with a 2km-long error message within the business logic.
2. What We Want to AVOID
Let's start by giving a bit of context to this article: imagine you're developing an simple social network, and in it, you need to create a simple post for your user.
src
├── Post
│ └── Post.php
└── User
├── User.php
└── Permission.php
Within this context, imagine you're trying to post as an user. Naturally, we'll impose some validation rules with their respective Exceptions
.
namespace Lukeskw\User;
use Lukeskw\Post\Post;
class User {
public function __construct(
public string $username,
public email $email,
protected Permission $permission
) {
}
public function post(Post $post): void
{
if (!$this->permission->hasPermission($this->username)) {
throw new \Exception(
'You do not have permission to post because you are a "' . $this->permission->name . '". '
);
}
if (strlen($post->description) < 50) {
throw new \Exception(
'You cannot post ' . $post->id . ' because it only has ' . strlen($post->description) . ' characters';
);
}
$this->postToFeed($post);
}
private function postToFeed(Post $post): void
{
// do awesome stuff
}
}
We've identified two validation rules that throw different exceptions to our client. And believe it or not, it works (in a scenario where the code is complete, ofc) and fulfills the validation role. HOWEVER, after learning that job interviews prioritize the QUALITY OF DELIVERY over the SPEED OF CREATION
, my perspective shifted slightly to understanding how to transform things that seem "strange and ugly" into "simple and elegant" things.
In this case, I'd really like to avoid two things:
- Generic Exceptions;
- Exceptions taking over places meant for business logic.
Don't get me wrong; the exceptions will still be where they are, but we'll improve the code's readability.
3. Refactoring 1: Creating Exceptions
src
├── Post
│ ├── Exceptions
│ │ └── PostException.php
│ └── Post.php
│
└── User
├── Exceptions
│ └── UserPermissionException.php
├── User.php
└── Permission.php
Alright, now let's move on to the part where we create our first "customized" Exception by extending the base Exception to a new class. It's nothing out of this world, but it already enhances our readability and understanding of the code in several aspects.
namespace Lukeskw\User;
class PostException extends \Exception
{}
class UserPermissionException extends \Exception
{}
And we'll do a simple refactoring in our post()
function, repositioning the default exceptions with the new exception we created.
namespace Lukeskw\User;
use Lukeskw\Post\Post;
use Lukeskw\Post\Exceptions\PostException;
use Lukeskw\User\Exceptions\UserPermissionException;
class User {
public function __construct(
public string $username,
public email $email,
protected Permission $permission
) {
}
public function post(Post $post): void
{
if (!$this->permission->hasPermission($this->username)) {
throw new UserPermissionException(
'You do not have permission to post because you are a "' . $this->permission->name . '". '
);
}
if (strlen($post->description) < 50) {
throw new PostException(
'You cannot post ' . $post->id . ' because it only has ' . strlen($post->description) . ' characters';
);
}
$this->postToFeed($post);
}
private function postToFeed(Post $post): void
{
// do awesome stuff
}
}
With the new Exceptions, we now know exactly what it's about and, most importantly, where to look in our codebase when this Exception is triggered. It's literally a CTRL + SHIFT + F
and search for the name "UserPermissionException." Makes your life easier, the life of the DevOps who will throw this into NewRelic/DataDog, and so on.
But something still bothers me a lot... Why are these giant messages in the middle of the business logic? Let's learn a way to hide this under the hood, but before that, we need to go through a Design Pattern called Factory!
4. Design Patterns: Factory Pattern
If you've heard of Design Patterns, you probably already understand a bit about what it solves. But in case you don't, I'll explain!
"Design Patterns are generic solutions to generic problems." - John Doe
In this idea of generic problems, some people got together and started creating some Software Design principles for you to solve day-to-day problems with a certain agility. Design Patterns are divided into three types:
- Behavioral Patterns;
- Creational Patterns;
- Structural Patterns.
You can read more about them on the Refactoring Guru website, and I highly recommend it for any developer to explore this documentation and self-develop. Okay, but let's focus on it, the so-called Creational Factory (or Factory Method).
The idea of this pattern is to create objects without having to instantiate a thousand things in different classes. You literally manufacture an instance of something and just receive it in a simple call to some function. In the programming world, there are hundreds of millions of calls like Models::make()
, Exception::create()
, ApiWhatever::factory()
so you don't have to access the constructor method of a respective class.
Taking the example of an API client, where we make the constructor modular in case we need to change the key and secret BUT still give the possibility of a quick call to manufacture the final object:
class GithubClient {
public function __construct(string $clientId, string $clientSecret)
{
$this->client = new Client([
'clientId' => $clientId,
'clientSecret' => $clientSecret,
]);
}
public static function make(): self
{
return new self(
env('github.client_id'),
env('github.client_secret'),
);
}
public function getUserByUsername(string $username = 'lukeskw'): array
{
// do a github call..
}
}
// Calling without creating object
$client = (new GithubClient('awesome-client-id', 'awesome-client-secret'))
->getUserByUsername('lukeskw');
// Using Factory
$client = GithubClient::make()
->getUserByUsername('lukeskw');
We made a static call, manufacturing all parameters in a succinct way. This "make/factory" or whatever you want to call it can be a very extensive method depending on what you're injecting, but that's not a problem.
But anyway, we saw that readability using the Factory Pattern was improved; of course, you can put better names for the functions, but at its core, that's it. Now let's go back to our exceptions!
5. Refactoring 2: Refining the Exceptions
Great, we've learned a bit about the factory; now let's apply it.
We'll create a factory method for our exception that makes sense in the context of what's happening. Yeah, none of that "make" or "create" in these moments. Exceptions need to tell at least a minimal story for the user or developer about what's happening, and that's what we'll focus on.
After a slight refactoring in our UserPermissionException
, we have the result of:
class UserPermissionException extends \Exception
{
public static function permissionNotFound(string $permission): self
{
$message = sprintf('You do not have permission to post because you are a "%s".', $permission);
return new self(
message: $message,
code: 403 // Forbidden
);
}
}
And after calling this factory in our code, we can notice an improvement in readability and maintainability since we isolated the information of the Exception within it.
public function post(Post $post): void
{
if (!$this->permission->hasPermission($this->username)) {
throw UserPermissionException::permissionNotFound($this->permission->name);
}
if (strlen($post->description) < 50) {
throw new PostException(
'You cannot post ' . $post->id . ' because it only has ' . strlen($post->description) . ' characters';
);
}
$this->postToFeed($post);
}
Now, refactoring the next one, we have the same idea of changing the PostException
.
class PostException extends \Exception
{
public static function minCharacters(string $postId, int $postCharCount): self
{
$message = sprintf(
'You cannot post %s because it only has %c characters', $postId, $postCharCount,
);
return new self(
message: $message,
code: 403 // Forbidden
);
}
}
And now our post()
is looking wonderful!
public function post(Post $post): void
{
if (!$this->permission->hasPermission($this->username)) {
throw UserPermissionException::permissionNotFound($this->permission->name);
}
if (strlen($post->description) < 50) {
throw PostException::minCharacters($post->id, strlen($post->description));
}
$this->postToFeed($post);
}
Is it wonderful? Yes. But there's still something bothering me... Why pass primitive types when these exceptions are "communicating" with classes?
It would be much cleaner if we pass the reference of the entire object to the Exception, and inside it, it resolves what it needs to use. After all, what if we need more things in the future, and it doesn't hurt to make it look nice, right?
namespace Lukeskw\Post;
use Lukeskw\Post\Post;
class PostException extends \Exception
{
public static function minCharacters(Post $post): self
{
$message = sprintf(
'You cannot post %s because it only has %c characters', $post->id, strlen($post->description),
);
return new self(
message: $message,
code: 403 // Forbidden
);
}
}
---------------------------------------------------------------
namespace Lukeskw\User;
use Lukeskw\User\Permission;
class UserPermissionException extends \Exception
{
public static function permissionNotFound(Permission $permission): self
{
$message = sprintf('You do not have permission to post because you are a "%s".', $permission->name);
return new self(
message: $message,
code: 403 // Forbidden
);
}
}
And the final result of our method is just charming, with encapsulated exceptions that will also give you great feedback in your job interview.
namespace Lukeskw\User;
use Lukeskw\Post\Post;
use Lukeskw\Post\Exceptions\PostException;
use Lukeskw\User\Exceptions\UserPermissionException;
class User {
public function __construct(
public string $username,
public email $email,
protected Permission $permission
) {
}
public function post(Post $post): void
{
if (!$this->permission->hasPermission($this->username)) {
throw UserPermissionException::permissionNotFound($this->permission);
}
if (strlen($post->description) < 50) {
throw PostException($post);
}
$this->postToFeed($post);
}
private function postToFeed(Post $post): void
{
// do awesome stuff
}
}
6. Conclusion
Exceptions are by far one of the most "annoying" things to deal with. After all, nobody wants errors popping up on the client's screen. But overall, they just need to be well-written, and adding a bit of charm with static calls, and BOOM, you get praise and a positive points in the job interview.
I really hope you liked this article, and don't forget to follow!
Top comments (1)
Such a incredible article!