Hi and welcome back for another article. I will give you my knowledge on my experience dealing with code that interrupt Laravel's default error handling, and how to mitigate issues that can occur when doing so.
- Context
- Configuring the error handler
- Why try-catch alone is an issue
- Solution #1: using
report()
- Solution #2: using
rescue()
- Conclusion
Context
Let us say your user is logged in its back-office, and can manage users. Your back-office user is allowed to create some users.
namespace App\Http\Controllers;
use App\Models\User;
use Illuminate\Http\Request;
class UserController
{
public function create()
{
return view("user.create");
}
public function store(Request $request)
{
User::create(["name" => $request->string("name")]);
return redirect()->route("user.index")->withSuccess("User stored.");
}
}
At some point, your database went down, and back-office users came to you and reported that they saw an "Oops - something went wrong" page.
You decide to make a custom validation error just so that users stay on the same page, and you still log the error to fix it later on.
namespace App\Http\Controllers;
use App\Models\User;
+ use Exception;
use Illuminate\Http\Request;
+ use Illuminate\Support\Facades\Log;
class UserController
{
public function create()
{
return view("user.create");
}
public function store(Request $request)
{
+ try {
User::create(["name" => $request->string("name")]);
+ } catch (Exception $exception) {
+ Log::error($exception->getMessage());
+
+ return redirect()->back()->withErrors("Unable to store the user.");
+ }
return redirect()->route("user.index")->withSuccess("User stored.");
}
}
Configuring the error handler
Let us imagine your error are not only logged on log file (by default), but also sent on Sentry to help debugging easily.
By the way, here is how an error renders on Sentry.
Here is how you have configured the file app/Exception/Handler.php
to do so.
namespace App\Exceptions;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Support\Facades\App;
use Throwable;
class Handler extends ExceptionHandler
{
/**
* A list of the exception types that are not reported.>
*/
protected $dontReport = [
//
];
protected $dontFlash = [
'current_password',
'password',
'password_confirmation',
];
public function register(): void
{
$this->reportable(function (Throwable $e) {
/**
* @see https://docs.sentry.io/platforms/php/guides/laravel/#install
*/
if (!App::environment('local') && $this->shouldReport($e) && app()->bound('sentry')) {
app('sentry')->captureException($e);
}
});
}
}
Why try-catch alone is an issue
Now that our error handler is modified, Laravel will automatically handle exception this way:
- Run through all error handlers (log, Sentry)
- Render the default error page ("Oops - something went wrong")
But since our code use try-catch, the exception is no longer thrown, plus we instruct Laravel to only send it on the file log.
This is where things are broken: you no longer benefit from Laravel's default error handling configuration, and the next time you want to figure out why the error was not reported to Sentry, you'll have an hard time remembering every place you interrupted Laravel's error handling.
Solution #1: using report()
This is the easiest, low effort solution to mitigate this issue.
Using report()
is designed to help you pass the exception instance through Laravel's error handling, but without rendering any response.
In our case it's perfect because we decided to bring our custom rendering when such error occurs, so let's implement it.
namespace App\Http\Controllers;
use App\Models\User;
use Exception;
use Illuminate\Http\Request;
- use Illuminate\Support\Facades\Log;
class UserController
{
public function create()
{
return view("user.create");
}
public function store(Request $request)
{
try {
User::create(["name" => $request->string("name")]);
} catch (Exception $exception) {
- Log::error($exception->getMessage());
+ report($exception);
return redirect()->back()->withErrors("Unable to store the user.");
}
return redirect()->route("user.index")->withSuccess("User stored.");
}
}
Next time an error is catched, it will be sent to the report error handler of Laravel, and the validation error will be return in the create user page, which preserves logging both on the log file and on Sentry.
Solution #2: using rescue()
This one is my favorite because the code becomes more readable.
Rescue will catch any exception that occurs, send it on the default exception handling mecanism, and allow you to return a custom value in this case.
namespace App\Http\Controllers;
use App\Models\User;
use Illuminate\Http\Request;
class UserController
{
public function create()
{
return view("user.create");
}
public function store(Request $request)
{
$stored = rescue(function () use ($request) {
User::create(["name" => $request->string("name")]);
return true;
}, false);
if (!$stored)
return redirect()->back()->withErrors("Unable to store the user.");
}
return redirect()->route("user.index")->withSuccess("User stored.");
}
}
As you can see, in first parameter, if the callback resolved successfully, rescue will return its value (true
in this case), and if any exception was catched, it will return the second parameter value (false
).
For the ones that like one liners, here is a condensed version.
namespace App\Http\Controllers;
use App\Models\User;
use Illuminate\Http\Request;
class UserController
{
public function create()
{
return view("user.create");
}
public function store(Request $request)
{
return rescue(function () use ($request) {
User::create(["name" => $request->string("name")]);
return redirect()->route("user.index")->withSuccess("User stored.");
}, function () {
return redirect()->back()->withErrors("Unable to store the user.");
});
}
}
One important note is that rescue()
will catch any exception that occurs, so if you have the need to send a specific message for a given exception, rescue()
will not be suited and you should probably opt for using the 1st solution.
namespace App\Http\Controllers;
use App\Models\User;
use Exception;
use Illuminate\Database\QueryException;
use Illuminate\Http\Request;
class UserController
{
public function create()
{
return view("user.create");
}
public function store(Request $request)
{
try {
User::create(["name" => $request->string("name")]);
} catch (QueryException $exception) {
report($exception);
return redirect()->back()->withErrors("Unable to store the user because the storage system did not respond.");
} catch (Exception $exception) {
report($exception);
return redirect()->back()->withErrors("Unable to store the user.");
}
return redirect()->route("user.index")->withSuccess("User stored.");
}
}
Conclusion
I hope this post was useful for you and that it will help you write more solid code given this could potentially help you being more aware of errors, mostly if your error handling mecanism use different logging solutions.
There is probably a few scenarios where neither report()
nor rescue()
would answer the need, so let me know if you have some example on the comment section!
That is all I have for today, I hope you will leave with new perspectives in mind after reading this article.
Happy error handling 🐞
Top comments (2)
Awesome, always though this would be very complicated but the framework makes it easy to handle this case. Thanks for sharing this tip!
Well written with easy to follow and clear examples. Thanks for the article. I learned some thing.