Quite often web-developers don't care enough about errors. If something goes wrong, you often see default Laravel texts like "Whoops, something went wrong" or, even worse, the exception code, which is not helpful at all to the visitor. So I decided to write a step-by-step article of how to handle errors in elegant way and present proper error information to the visitor.
As a side-note: this article will also show an example of creating your own Service with Dependency Injection, and handling exceptions thrown by the service.
Preparation: User Search Task
So we have a really simple example - a form to find a user by their ID.
We have two routes:
Route::get('/users', [UserController::class, 'index'])->name('users.index'); Route::post('/users/search', [UserController::class, 'search'])->name('users.search');
And a controller with two methods:
class UserController extends Controller { public function index() { return view('users.index'); } public function search(Request $request) { $user = User::find($request->input('user_id')); return view('users.search', compact('user')); } }
Finally, resources/views/users/index.blade.php will present a form:
<form action="{{ route('users.search') }}" method="POST">
@csrf
<div class="form-group">
<input id="user_id" class="form-control" name="user_id" type="text" value="{{ old('user_id') }}" placeholder="User ID">
</div>
<input class="btn btn-info" type="submit" value="Search">
</form>
If we search for existing user and it's found, we see this result:
It's all in resources/views/users/search.blade.php:
<h3 class="text-center page-title">User found: {{ $user->name }}</h3>
<b>Email</b>: {{ $user->email }}
<br>
<b>Registered on</b>: {{ $user->created_at }}
Ok, so this is our ideal scenario. But what if the user is not found?
Exception handling
Let's get out of ideal world. We're not checking for user existence, we're only doing this in Controller:
$user = User::find($request->input('user_id'));
And if user is not found, we will see this:
Or, of course, we can set .env file with APP_DEBUG=false and then the browser will just show blank Whoops, looks like something went wrong. But that still doesn't give any valuable information to our visitor.
Another quick fix we can make is using User::findOrFail() instead of just find() - then if user is not found, Laravel would show 404 page with text "Sorry, the page you are looking for could not be found.". But this is a default 404 page for the whole project, so not massively helpful to user, is it?
So we need to catch the errors, process them and redirect back to the form with actual understandable error message.
We need to know the exception type and class name that it would return. In case of findOrFail() it would throw an Eloquent exception ModelNotFoundException, so we need to do this:
public function search(Request $request) { try { $user = User::findOrFail($request->input('user_id')); } catch (ModelNotFoundException $exception) { return back()->withError($exception->getMessage())->withInput(); } return view('users.search', compact('user')); }
Now, let's actually show an error in Blade:
<h3 class="text-center page-title">Search for user by ID</h3>
@if (session('error'))
<div class="alert alert-danger">{{ session('error') }}</div>
@endif
<form action="{{ route('users.search') }}" method="POST">...</form>
Result:
Great, we show the error message! But it's still not ideal, right? Instead of $exception->getMessage() we need to show our own message:
return back()->withError('User not found by ID ' . $request->input('user_id'))->withInput();
Finally!
Moving Error Message Handling into Service
For now, we've taken a really simple example of one action in controller - just finding the user. In real application it gets more complicated - usually controller is calling some kind of external service or package method which may fail with different kind of errors.
Let's create our own service which would essentially do the same thing, but would throw exception, so controller wouldn't even have to know the message text.
Let's move our logic into app/Services/UserService.php:
namespace App\Services; use App\User; use Illuminate\Database\Eloquent\ModelNotFoundException; class UserService { public function search($user_id) { $user = User::find($user_id); if (!$user) { throw new ModelNotFoundException('User not found by ID ' . $user_id); } return $user; } }
And in Controller, we need to call this service. First, we inject it into __construct() method:
use App\Services\UserService; class UserController extends Controller { private $userService; public function __construct(UserService $userService) { $this->userService = $userService; } // ...
If you are not familiar with dependency injection and how Laravel IOC container works, here's official documentation or a good article about it.
Now, here's how our search() method looks:
public function search(Request $request) { try { $user = $this->userService->search($request->input('user_id')); } catch (ModelNotFoundException $exception) { return back()->withError($exception->getMessage())->withInput(); } return view('users.search', compact('user')); }
Notice that we can use $exception->getMessage() again, and all the error validation or message logic is happening within the service - that's one of the purpose, to separate these actions, controller shouldn't perform it.
Step Even Further: Creating Our Own Exception Class
Final chapter in this article - even better architecture when your service throws its own exception related to that particular error, and there could be multiple exception classes depending on error. A good example of such architecture is Stripe library, its usage looks like this:
try { // Use Stripe's library to make requests... } catch(\Stripe\Error\Card $e) { // Since it's a decline, \Stripe\Error\Card will be caught $body = $e->getJsonBody(); $err = $body['error']; print('Status is:' . $e->getHttpStatus() . "\n"); print('Type is:' . $err['type'] . "\n"); print('Code is:' . $err['code'] . "\n"); // param is '' in this case print('Param is:' . $err['param'] . "\n"); print('Message is:' . $err['message'] . "\n"); } catch (\Stripe\Error\RateLimit $e) { // Too many requests made to the API too quickly } catch (\Stripe\Error\InvalidRequest $e) { // Invalid parameters were supplied to Stripe's API } catch (\Stripe\Error\Authentication $e) { // Authentication with Stripe's API failed // (maybe you changed API keys recently) } catch (\Stripe\Error\ApiConnection $e) { // Network communication with Stripe failed } catch (\Stripe\Error\Base $e) { // Display a very generic error to the user, and maybe send // yourself an email } catch (Exception $e) { // Something else happened, completely unrelated to Stripe }
So how can we create our own exception class? Simple, with Artisan command:
php artisan make:exception UserNotFoundException
Here's what it would generate in app/Exceptions/UserNotFoundException.php:
namespace App\Exceptions; use Exception; class UserNotFoundException extends Exception { // }
Nothing here, right? Let's fill our exception with some logic.
There could be two methods in this class:
- report() is used if you want to do some additional logging - send error to BugSnag, email, Slack etc.
- render() is used if you want to redirect back with error or return HTTP response (like your own Blade file) directly from Exception class
So, for this example we fill our render() method:
namespace App\Exceptions; use Exception; class UserNotFoundException extends Exception { /** * Report or log an exception. * * @return void */ public function report() { \Log::debug('User not found'); } }
Finally, this is how we call this exception then, from Controller:
public function search(Request $request) { try { $user = $this->userService->search($request->input('user_id')); } catch (UserNotFoundException $exception) { report($exception); return back()->withError($exception->getMessage())->withInput(); } return view('users.search', compact('user')); }
So, that's all I wanted to show you about Exception handling and also using Services, as a side-note.
I understand that my example is really simplified, and other people could use Exceptions in a different way, but I hope this article will give an overview of exceptions in general, and reasons WHY you should use them, to show errors to the visitors in elegant way.
More about exceptions and error handling: official Laravel documentation
Video example
I've shot a video for new online-course, which demonstrates some things related to exceptions:
So, for this example we fill our render() method:
but in code
report
is used:` namespace App\Exceptions;
use Exception;
class UserNotFoundException extends Exception { /** * Report or log an exception. * * @return void */ public function report() { \Log::debug('User not found'); } } `