Try-Catch in Laravel: WHEN to Use it? 10+ Practical Examples.

Try-Catch in Laravel: WHEN to Use it? 10+ Practical Examples.
Admin
Saturday, August 10, 2024 6 mins to read
Share
Try-Catch in Laravel: WHEN to Use it? 10+ Practical Examples.

The try-catch PHP operator is very old, and we all know its syntax. But the confusing part is WHEN to use it. In what cases? In this tutorial, I will show practical examples to explain the answer.

Notice: this article is a try-catch "practical summary", but if you want to dive deeper, we have a separate longer course Handling Exceptions and Errors in Laravel.


First Pair of Examples: Eloquent VS JSON Decode

Let me explain the try-catch with two opposite examples.

Example 1: Eloquent. Take a look at this piece of code.

try {
User::create($request->validated());
} catch (Exception $e) {
return redirect()->route('users.create')
->with('error', 'Operation failed: ' . $e->getMessage());
}

It's a valid code in terms of syntax, but what is the probability of User::create() throwing any Exception? Pretty low, right?

Also, if the data is invalid, it should be caught earlier, in the Validation phase.

Example 2: JSON. Compare it to this example from the Laravel framework core:

/**
* Determine if a given value is valid JSON.
*
* @param mixed $value
* @return bool
*/
public static function isJson($value)
{
// ... some more code
 
try {
json_decode($value, true, 512, JSON_THROW_ON_ERROR);
} catch (JsonException) {
return false;
}
 
return true;
}

Do you feel what is fundamentally different in this example? Two things:

  1. We cannot "trust" the json_decode() method because it was not created by us. We also can't trust the $value because it may come from different user input. So, there's a high chance of an Exception happening.
  2. Also, we want to specify what to do in case of that Exception: instead of showing the error on the screen, we tell that JSON is not recognized and return false. Then, whoever calls that isJson() method decides what to do next.

If you don't feel the difference, I will try to rephrase it.


Three MAIN Conditions to Use Try-Catch

Based on these examples above, here's how I would summarize, in what cases you should mostly use try-catch.

  1. You Call "Risky" Code: when your main code is an operation you don't fully control and is likely to throw an Exception. Examples: third-party APIs, external packages, filesystem operations.
  2. You've Already Validated Data: when the existing Laravel/PHP validation mechanism doesn't catch that Exception for you.
  3. You EXPECT the Exception: When you actually have a plan for what to do in case of that Exception, usually when catching a specific Exception.

In human analogy, it's like trying to drive from A to B without a map, with roughly knowing the route and the destination. Then, you TRY to drive there but prepare a plan B (insurance?) in case you get lost or crash somewhere.

I know, I know, maybe not the best analogy, but you get the idea :)

In other words, try-catch is used as an "insurance policy" in case of a higher risk of failure. That's why you should use it in specific cases when you actually need it. You wouldn't buy the accident insurance if you just go shopping near home, right?


What Do You Do in "catch"? 7+ Examples.

If you use try-catch, you need to have a plan of what you do in case of Exception. So, it's time to look at more examples.

I've prepared a list of 7 different goals you may want to accomplish in case of exceptions happening.

The code examples come from the Laravel core framework itself. Let's learn from the best.


Goal 1. Return false/NULL or other "fallback" value

The most typical case I've found is when developers want to assign a default value to the method return, if something goes wrong. See this example:

src/Illuminate/Auth/Access/Gate.php:

class Gate implements GateContract
{
// ...
 
protected function methodAllowsGuests($class, $method)
{
try {
$reflection = new ReflectionClass($class);
 
$method = $reflection->getMethod($method);
} catch (Exception) {
return false;
}
 
if ($method) {
$parameters = $method->getParameters();
 
return isset($parameters[0]) && $this->parameterAllowsGuests($parameters[0]);
}
 
return false;
}

This method should return a bool value, which may be true only if the parameter is valid.

It returns false in all the other cases:

  • If the underlying parameterAllowsGuests() returns false
  • If the $method is empty
  • Or... if ANY Exception happens when getting that method

Also, that's an interesting example of so-called early return. So, the try-catch mechanism doesn't necessarily have to cover the entire function; it may be used only in a specific part.

Another similar example comes from Validation functionality of Laravel:

src/Illuminate/Validation/Concerns/ValidatesAttributes.php:

trait ValidatesAttributes
{
public function validateActiveUrl($attribute, $value)
{
if (! is_string($value)) {
return false;
}
 
if ($url = parse_url($value, PHP_URL_HOST)) {
try {
$records = $this->getDnsRecords($url.'.', DNS_A | DNS_AAAA);
 
if (is_array($records) && count($records) > 0) {
return true;
}
} catch (Exception) {
return false;
}
}
 
return false;
}

Also, we don't necessarily catch "any" Exception. We may expect a specific one. Look at this example of Enum validation with expected TypeError:

src/Illuminate/Validation/Rules/Enum.php:

class Enum implements Rule, ValidatorAwareRule
{
public function passes($attribute, $value)
{
if ($value instanceof $this->type) {
return $this->isDesirable($value);
}
 
if (is_null($value) || ! enum_exists($this->type) || ! method_exists($this->type, 'tryFrom')) {
return false;
}
 
try {
$value = $this->type::tryFrom($value);
 
return ! is_null($value) && $this->isDesirable($value);
} catch (TypeError) {
return false;
}
}

Goal 2. Show "Human-Friendly" Error Message

This is also a typical use-case: you may want to return your own styled error message/page in case of Exception.

See this example from Artisan command php artisan down:

src/Illuminate/Foundation/Console/DownCommand.php:

class DownCommand extends Command
{
public function handle()
{
try {
// ... I deliberately skip some code for simplicity
 
file_put_contents(
storage_path('framework/maintenance.php'),
file_get_contents(__DIR__.'/stubs/maintenance-mode.stub')
);
 
$this->components->info('Application is now in maintenance mode.');
} catch (Exception $e) {
$this->components->error(sprintf(
'Failed to enter maintenance mode: %s.',
$e->getMessage(),
));
 
return 1;
}
}
}

Two things are happening here in case of Exception:

  • Showing a more human-friendly error message
  • Return 1 as a convention, which means failed Artisan command

Also, as you can see, the operation itself is a "more risky" function file_put_contents(): we're not sure if that storage folder even exists and is writeable. That's why it makes sense to use try-catch in the first place.


Goal 3. Replace Exception With Your Custom Exception

Maybe you want to catch a specific PHP/Laravel Exception and replace it with your own custom Exception that handles error handling in your way.

This example comes from Eloquent Relationships:

https://github.com/laravel/framework/blob/6c07bcaa0784c8bf751e9f78cf805cbf2a6cdbc3/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php#L569C1-L573C10

try {
$relationship = $this->model->{$relationshipName}();
} catch (BadMethodCallException) {
throw RelationNotFoundException::make($this->model, $relationshipName);
}

In this case, we're trying to replace the PHP BadMethodCallException with Laravel's RelationNotFoundException that would show a more human-friendly message of "Call to undefined relationship [{$relation}] on model [{$class}]."

You may learn more about creating Custom Exceptions in our specific course lesson.


Goal 4. Perform Action Before Exception is Thrown

Sometimes, we may want to perform some "cleanup" before the Exception is actually executed and our script is terminated.

This example comes from Blade View rendering mechanism:

src/Illuminate/View/View.php:

class View implements ArrayAccess, Htmlable, Stringable, ViewContract
{
public function render(?callable $callback = null)
{
try {
$contents = $this->renderContents();
 
$response = isset($callback) ? $callback($this, $contents) : null;
 
// Once we have the contents of the view, we will flush the sections if we are
// done rendering all views so that there is nothing left hanging over when
// another view gets rendered in the future by the application developer.
$this->factory->flushStateIfDoneRendering();
 
return ! is_null($response) ? $response : $contents;
} catch (Throwable $e) {
$this->factory->flushState();
 
throw $e;
}
}
}

The operation of flushState() must be performed regardless of whether the render was successful.

By the way, see that Throwable? It's not an Exception, is it?

The thing is that there's a difference between PHP Exceptions and PHP Errors.

Examples of errors may be:

  • Wrong argument count for function
  • Division by zero
  • TypeError (you already saw that above)
  • ... and more, see the full list on the right sidebar of this page

So, a Throwable was introduced as the base interface for any object that can be thrown via a throw statement, including BOTH Error and Exception.

In Laravel framework core, you may find many places where it tries to catch Throwable to cover both Exceptions and PHP errors.


Goal 5. Add Extra Data To Exception

As a separate sub-case of "do something before Exception is executed", we may also add something to that Exception class.

This example comes from Validation with Error Bag:

src/Illuminate/Validation/Validator.php:

class Validator implements ValidatorContract
{
public function validateWithBag(string $errorBag)
{
try {
return $this->validate();
} catch (ValidationException $e) {
$e->errorBag = $errorBag;
 
throw $e;
}
}

As you can see, we're just passing the parameter to the Exception class.


Goal 6. Just Let Exception Occur

I've seen this code many times, especially by junior developers: using try-catch but not actually doing anything in the catch block.

I will be honest: in the past, I considered it a "bad practice". But, over the years, I have encountered many cases where it just makes sense.

So, you expect some Exception to appear but want to ignore it, saying something like "Ok, ok, I know that something went wrong, but I still want my script to continue".

This example comes from matching the routes:

src/Illuminate/Routing/CompiledRouteCollection.php:

class CompiledRouteCollection extends AbstractRouteCollection
{
/**
* Find the first route matching a given request.
*/
public function match(Request $request)
{
$matcher = new CompiledUrlMatcher(
$this->compiled, (new RequestContext)->fromRequest(
$trimmedRequest = $this->requestWithoutTrailingSlash($request)
)
);
 
$route = null;
 
try {
if ($result = $matcher->matchRequest($trimmedRequest)) {
$route = $this->getByName($result['_route']);
}
} catch (ResourceNotFoundException|MethodNotAllowedException) {
try {
return $this->routes->match($request);
} catch (NotFoundHttpException) {
//
}
}
 
if ($route && $route->isFallback) {
try {
$dynamicRoute = $this->routes->match($request);
 
if (! $dynamicRoute->isFallback) {
$route = $dynamicRoute;
}
} catch (NotFoundHttpException|MethodNotAllowedHttpException) {
//
}
}
 
return $this->handleMatchedRoute($request, $route);
}

Two things to emphasize in this example:

  • In case of Exceptions happening, the script will still continue to execute the final handleMatchedRoute() method at the bottom
  • Have you noticed you can do another try-catch in the catch block of the "main" try-catch?

Goal 7. Just Log the Exception.

This use case is debatable. I often see the Log::xxxxx() in the catch block.

This is a typical example:

try {
// Code that may throw an Exception
} catch (Exception $e) {
// Log the message locally
Log::debug($e->getMessage());
 
// Friendlier message to display to the user
// OR redirect them to a failure page
}

But personally, I disapprove of doing try-catch manually just for logging. I would probably use specific external tools like Sentry or BugSnag: they catch Exceptions automatically and group them for processing in a much more convenient way later.


(Bonus) Goal 8. Try-catch... FINALLY?

You don't see it very often, so I consider it a "bonus", but there's also the third part of try-catch.

This example comes from Artisan Console component:

src/Illuminate/Console/View/Components/Task.php:

class Task extends Component
{
public function render($description, $task = null, $verbosity = OutputInterface::VERBOSITY_NORMAL)
{
// ... skipped some code for simplicity
 
try {
$result = ($task ?: fn () => true)();
} catch (Throwable $e) {
throw $e;
} finally {
$runTime = $task
? (' '.$this->runTimeForHumans($startTime))
: '';
 
$runTimeWidth = mb_strlen($runTime);
$width = min(terminal()->width(), 150);
$dots = max($width - $descriptionWidth - $runTimeWidth - 10, 0);
 
$this->output->write(str_repeat('<fg=gray>.</>', $dots), false, $verbosity);
$this->output->write("<fg=gray>$runTime</>", false, $verbosity);
 
$this->output->writeln(
$result !== false ? ' <fg=green;options=bold>DONE</>' : ' <fg=red;options=bold>FAIL</>',
$verbosity,
);
}
}

See this HUGE block of finally? In human language, it's this: no matter if the try block succeeds or fails, the result will still be shown on the screen to the user, with runtime duration numbers.


So, If Not Try-Catch... Then What?

Whew.

We've covered A LOT of different examples, focusing on goals explaining when you may want to use try-catch. However, what if none of them applies, but you still want to make your code more error-proof?

As mentioned in the very beginning of the article, the majority of errors in Laravel should be caught by the internal mechanism of the framework, like these:

  • Framework Methods: functions like User::findOrFail() or Route-Model Binding will automatically throw a 404 page or HTTP status code
  • Validation: with $request->validate() or Form Request classes - the framework will automatically redirect back to the form or return 422 HTTP status code
  • Middleware: catch the invalid request parameters even BEFORE it gets to the Controller

From that angle, try-catch should be used as a "fallback" option when all the tools above are appropriately used. After all, that's one of the reasons we use a framework like Laravel: to take care of some things automatically for us.

What do you think? Has this article given you more clarity?