Logging and error pages
A visitor of a live site is not shown an error, but the developer must learn about it. Symfony writes errors to a log through the Monolog library and gives the visitor a page that you can style to match your site.
Writing to the log
use Psr\Log\LoggerInterface;
#[Route('/order/{id}/pay', name: 'order_pay', methods: ['POST'])]
public function pay(Order $order, LoggerInterface $logger): Response
{
$logger->info('Payment started', ['order_id' => $order->getId()]);
// …
$logger->error('The payment service is unavailable', ['order_id' => $order->getId()]);
// …
}The second argument is an array of details. During development entries go to var/log/dev.log and are visible in the profiler.
Levels
| Level | When to use it |
|---|---|
debug | Details for debugging |
info | Ordinary events |
warning | Something unusual, but work continues |
error | An operation failed |
critical | An important part of the system is down |
Configuration
config/packages/monolog.yaml decides what is written and where:
when@prod:
monolog:
handlers:
main:
type: fingers_crossed
action_level: error
handler: file
file:
type: rotating_file
path: '%kernel.logs_dir%/%kernel.environment%.log'
max_files: 14fingers_crossed collects every message of a request and writes them only if an error happened: the log stays small, yet the whole history sits next to the error. rotating_file starts a new file each day and keeps the last 14.
Custom error pages
The templates go into a special folder:
{# templates/bundles/TwigBundle/Exception/error404.html.twig #}
{% extends 'base.html.twig' %}
{% block body %}
<h1>Page not found</h1>
<a href="{{ path('home') }}">Go to the home page</a>
{% endblock %}error404.html.twig is for code 404, error403.html.twig for 403, error.html.twig for all the rest.
Previewing during development
In development mode a detailed report is shown instead of these pages. To see your own page, open the service address:
http://localhost:8000/_error/404Raising an error
throw $this->createNotFoundException('Post not found');
throw $this->createAccessDeniedException();
throw new \Symfony\Component\HttpKernel\Exception\HttpException(429, 'Too many requests');Errors as JSON
An HTML page does not suit an API. A listener turns an exception into a JSON response:
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
#[AsEventListener]
class ApiExceptionListener
{
public function __invoke(ExceptionEvent $event): void
{
if (!str_starts_with($event->getRequest()->getPathInfo(), '/api')) {
return;
}
$e = $event->getThrowable();
$status = $e instanceof HttpExceptionInterface ? $e->getStatusCode() : 500;
$event->setResponse(new JsonResponse(['error' => $e->getMessage()], $status));
}
}Common mistake
The exception text must not be given to a visitor on a 500 error: it may contain table names, file paths and fragments of queries. In the example above it is better to return a general phrase for code 500 and leave the details to the log. For the same reason passwords, tokens and card numbers are not written to the log — only identifiers.