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.
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:
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.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.
Based on these examples above, here's how I would summarize, in what cases you should mostly use try-catch.
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?
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.
false
/NULL
or other "fallback" valueThe 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:
parameterAllowsGuests()
returns false
$method
is emptyAlso, 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; } }
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:
1
as a convention, which means failed Artisan commandAlso, 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.
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:
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.
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:
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.
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.
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:
handleMatchedRoute()
method at the bottomtry-catch
in the catch
block of the "main" try-catch
?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.
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.
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:
User::findOrFail()
or Route-Model Binding will automatically throw a 404 page or HTTP status code$request->validate()
or Form Request classes - the framework will automatically redirect back to the form or return 422 HTTP status codeFrom 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?