Back to Course |
Handling Exceptions and Errors in Laravel

Creating Your Custom Exceptions

Look at a typical error message from a general Carbon Exception:

Wouldn't it be better if we showed a more specific error message?

We can build and catch custom exceptions for this.


WHY You Should Create Custom Exceptions

Here are possible reasons:

  • Clearer Exception purpose. You can name the Exception class based on the action being performed, for example, ImportHasMalformedData or UserAlreadyExists.
  • Better error messages. You can provide a more actionable message to the developer, which will help them to fix the issue faster. For example, The import has malformed data. or The user already exists..
  • More flexible error handling. You can catch the Exception and handle it accordingly. For example, redirect the user to a different page, log the error, etc.
  • Classes vs Strings. Your code will throw Exceptions rather than returning a random string with a value (see example below).
  • More structured and readable code. Your code will have more structure, and understanding/using it will be easier for other developers.
  • External tools. As a bonus, using Exceptions can improve your error handling using tools like Bugsnag, Sentry, Flare, etc., as they will log all the information needed for faster bug fixes!

To illustrate this, let's look at the following code:

$error = 'OK';
 
function workWithFile($file) {
// Reads the File...
// Works with File...
// Possibly has an error inside.
}
 
$result = workWithFile('PATH_TO_FILE');
 
if ($result == 'NOT_FOUND') {
deleteFriendListFromMenu();
} elseif ($result == 'NOT_READABLE') {
alertUserAboutPermissionProblem();
} elseif ($result == 'OK') {
return $result; // File was read okay!
} else {
die("Something bad happened. Not in the list")
}

Looking at this code, we might understand what is going on, but there are a lot of hard-coded strings. So, we need to remember those strings' format and avoid making any typos in them.

If we make a typo somewhere, PHP/Laravel/IDE won't immediately flag it as a bug.

Using Exceptions can be a better experience:

function workWithFile($file) {
if (!is_file($file)) {
throw new FileNotFoundException($file);
} else if (!is_readable($file)) {
throw new FileNotReadableException($file);
} else {
// Reads the File...
}
}
 
try {
$fileContents = workWithFile('PATH_TO_FILE');
 
return $fileContents;
} catch (FileNotFoundException $e) {
//File not found, we can log it, alert the user, etc.
} catch (FileNotReadableException $e) {
//File could not be read; we can log it, alert the user, etc.
}

As you can see, we dropped quite a few if conditions and instead used different exceptions to handle the errors. This makes the code more readable and easier to understand, especially if combined with custom exceptions.

Also, if we make a typo in the class name, PHP/IDE will immediately flag it as a bug before the code is pushed to live.


Creating Custom Exceptions

To create a custom Exception in Laravel, you need to run a single command:

php artisan make:exception ImportHasMalformedData

It will create a new file in app/Exceptions/ImportHasMalformedData.php:

app/Exceptions/ImportHasMalformedData.php

class ImportHasMalformedData extends Exception
{
protected $message = 'The import has malformed data.';
}

Now throwing this Exception will result in a more actionable message:

This Exception can now be caught by the catch and handled accordingly. For example, we'll display a message to the user using this code:

use App\Exceptions\ImportHasMalformedData;
 
// ...
 
public function __invoke()
{
$data = [['name' => 'John Doe', 'email' => 'email@email.com', 'date' => '23-04']];
 
try {
$this->import($data);
} catch (ImportHasMalformedData $e) {
return 'Malformed data';
}
 
return 'Imported';
}

Running this, we will have the following output in our browser:

Malformed data

To go even further, we can add more exceptions to our code that will better describe the problem:

php artisan make:exception ImportFailedToParseDate
php artisan make:exception ImportFailedToParseEmail
php artisan make:exception ImportHasNoName
php artisan make:exception ImportHasNoCompany
php artisan make:exception ImportHasEmptyRow

Now, we can use them in our code:

use App\Exceptions\ImportFailedToParseDate;
use App\Exceptions\ImportFailedToParseEmail;
use App\Exceptions\ImportHasNoCompany;
use App\Exceptions\ImportHasNoName;
use App\Exceptions\ImportHasEmptyRow;
 
// ...
 
public function __invoke() {
// Load the file
 
$failedRows = [];
 
foreach($fileData as $row) {
try {
$this->importRow($row);
} catch (ImportFailedToParseDate $e) {
// Handle the error
$failedRows[get_class($e)][] = $row;
} catch (ImportFailedToParseEmail $e) {
// Handle the error
$failedRows[get_class($e)][] = $row;
} catch (ImportHasNoName $e) {
// Handle the error
$failedRows[get_class($e)][] = $row;
} catch (ImportHasNoCompany $e) {
// Handle the error
$failedRows[get_class($e)][] = $row;
} catch (ImportHasEmptyRow $e) {
// Handle the error
continue; // Skip this row and continue with the next one
} catch (Exception $e) {
$failedRows[$e->getMessage()][] = $row;
// Handle any other errors
}
}
 
// Email user with failed rows (or display preview)
// ...
 
return 'Imported';
}
 
private function importRow(array $rowData) {
if (empty($rowData)) {
throw new ImportHasEmptyRow();
}
 
if (!isset($rowData['name'])) {
throw new ImportHasNoName();
}
 
if (!isset($rowData['company'])) {
throw new ImportHasNoCompany();
}
 
if (!isset($rowData['email'])) {
throw new ImportFailedToParseEmail();
}
 
if (!isset($rowData['date'])) {
throw new ImportFailedToParseDate();
}
 
try {
$date = Carbon::parse($rowData['date']);
} catch (Exception $e){
throw new ImportFailedToParseDate(previous: $e);
}
 
// Import the row to the database
}

This allows us to have a more structured code and quickly understand what is happening. Especially since we know that if any file contents are malformed, we will get an Exception with specific information about the problem.

Notice: You may do different things in the catch block, like redirecting the user to a separate page, logging the error, etc.