Manejo de Errores en Producción
En producción, los errores no deben mostrarse al usuario pero sí registrarse para poder diagnosticar problemas. Aprende a configurar handlers globales, logging y páginas de error amigables.
Configuración para producción
La regla principal: nunca mostrar errores al usuario. Los mensajes de error pueden revelar información sensible sobre tu aplicación.
<?php
declare(strict_types=1);
// bootstrap.php o config.php
$esProduccion = getenv('APP_ENV') === 'production';
if ($esProduccion) {
// PRODUCCIÓN: ocultar errores, registrar en log
error_reporting(E_ALL);
ini_set('display_errors', '0');
ini_set('display_startup_errors', '0');
ini_set('log_errors', '1');
ini_set('error_log', '/var/log/php/app-errors.log');
} else {
// DESARROLLO: mostrar todo
error_reporting(E_ALL);
ini_set('display_errors', '1');
ini_set('display_startup_errors', '1');
}
Handler global de excepciones
set_exception_handler captura
cualquier excepción no manejada antes de que PHP
muestre el error al usuario:
<?php
declare(strict_types=1);
set_exception_handler(function (Throwable $e): void {
// 1. Registrar el error completo en el log
$mensaje = sprintf(
"[%s] %s: %s en %s:%d\nStack trace:\n%s",
date('Y-m-d H:i:s'),
get_class($e),
$e->getMessage(),
$e->getFile(),
$e->getLine(),
$e->getTraceAsString()
);
error_log($mensaje);
// 2. Mostrar página de error genérica al usuario
http_response_code(500);
if (getenv('APP_ENV') === 'production') {
// Página amigable sin detalles técnicos
include __DIR__ . '/templates/error-500.html';
} else {
// En desarrollo, mostrar el error completo
echo "<h1>Error: " . htmlspecialchars($e->getMessage()) . "</h1>";
echo "<pre>" . htmlspecialchars($e->getTraceAsString()) . "</pre>";
}
exit(1);
});
// Ahora cualquier excepción no capturada pasará por este handler
throw new RuntimeException('Error de ejemplo');
Handler global de errores
set_error_handler captura errores
tradicionales (warnings, notices) y los
convierte en excepciones para manejarlos
uniformemente:
<?php
declare(strict_types=1);
set_error_handler(function (
int $nivel,
string $mensaje,
string $archivo,
int $linea
): bool {
// Respetar el nivel de error_reporting
if (!(error_reporting() & $nivel)) {
return false;
}
// Convertir el error en excepción
throw new ErrorException($mensaje, 0, $nivel, $archivo, $linea);
});
// Ahora los warnings también se capturan como excepciones
try {
$resultado = 1 / 0; // Antes era warning, ahora es excepción
} catch (ErrorException $e) {
echo 'Capturado: ' . $e->getMessage();
}
Función de shutdown
register_shutdown_function se
ejecuta cuando el script termina, incluyendo
errores fatales que no pueden capturarse de otra
forma:
<?php
declare(strict_types=1);
register_shutdown_function(function (): void {
$error = error_get_last();
if ($error === null) {
return; // No hubo error
}
// Solo nos interesan errores fatales
$fatales = [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR];
if (!in_array($error['type'], $fatales, true)) {
return;
}
// Registrar el error fatal
$mensaje = sprintf(
"[FATAL] %s en %s:%d",
$error['message'],
$error['file'],
$error['line']
);
error_log($mensaje);
// Si no se han enviado headers, mostrar página de error
if (!headers_sent()) {
http_response_code(500);
include __DIR__ . '/templates/error-500.html';
}
});
Clase ErrorHandler completa
Combinar todo en una clase reutilizable:
<?php
declare(strict_types=1);
class ErrorHandler
{
private bool $debug;
private string $logPath;
public function __construct(bool $debug = false, string $logPath = '')
{
$this->debug = $debug;
$this->logPath = $logPath ?: sys_get_temp_dir() . '/app.log';
}
public function register(): void
{
set_error_handler([$this, 'handleError']);
set_exception_handler([$this, 'handleException']);
register_shutdown_function([$this, 'handleShutdown']);
}
public function handleError(
int $nivel,
string $mensaje,
string $archivo,
int $linea
): bool {
if (!(error_reporting() & $nivel)) {
return false;
}
throw new ErrorException($mensaje, 0, $nivel, $archivo, $linea);
}
public function handleException(Throwable $e): void
{
$this->log($e);
$this->render($e);
exit(1);
}
public function handleShutdown(): void
{
$error = error_get_last();
if ($error === null) {
return;
}
$fatales = [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR];
if (in_array($error['type'], $fatales, true)) {
$e = new ErrorException(
$error['message'],
0,
$error['type'],
$error['file'],
$error['line']
);
$this->handleException($e);
}
}
private function log(Throwable $e): void
{
$mensaje = sprintf(
"[%s] %s: %s en %s:%d\n%s\n\n",
date('Y-m-d H:i:s'),
get_class($e),
$e->getMessage(),
$e->getFile(),
$e->getLine(),
$e->getTraceAsString()
);
file_put_contents($this->logPath, $mensaje, FILE_APPEND | LOCK_EX);
}
private function render(Throwable $e): void
{
if (!headers_sent()) {
http_response_code(500);
header('Content-Type: text/html; charset=utf-8');
}
if ($this->debug) {
$this->renderDebug($e);
} else {
$this->renderProduction();
}
}
private function renderDebug(Throwable $e): void
{
echo '<!DOCTYPE html><html><head><title>Error</title></head><body>';
echo '<h1>' . htmlspecialchars(get_class($e)) . '</h1>';
echo '<p>' . htmlspecialchars($e->getMessage()) . '</p>';
echo '<p><strong>Archivo:</strong> ' . $e->getFile() . ':' . $e->getLine() . '</p>';
echo '<pre>' . htmlspecialchars($e->getTraceAsString()) . '</pre>';
echo '</body></html>';
}
private function renderProduction(): void
{
echo '<!DOCTYPE html><html><head><title>Error</title></head><body>';
echo '<h1>Ha ocurrido un error</h1>';
echo '<p>Lo sentimos, algo salió mal. Por favor, inténtalo de nuevo más tarde.</p>';
echo '</body></html>';
}
}
// Uso
$handler = new ErrorHandler(
debug: getenv('APP_ENV') !== 'production',
logPath: '/var/log/app/errors.log'
);
$handler->register();
Logging con error_log
PHP tiene funciones nativas para registrar errores:
<?php
declare(strict_types=1);
// Escribir al log configurado en php.ini
error_log('Mensaje de error');
// Escribir a un archivo específico
error_log('Error en pago', 3, '/var/log/pagos.log');
// Enviar por email (tipo 1) - poco usado
error_log('Error crítico', 1, 'admin@example.com');
// Función helper para logging estructurado
function logError(string $nivel, string $mensaje, array $contexto = []): void
{
$linea = sprintf(
"[%s] [%s] %s %s\n",
date('Y-m-d H:i:s'),
strtoupper($nivel),
$mensaje,
$contexto !== [] ? json_encode($contexto) : ''
);
error_log($linea, 3, '/var/log/app/app.log');
}
// Uso
logError('error', 'Pago fallido', [
'usuario_id' => 123,
'monto' => 99.99,
'codigo' => 'CARD_DECLINED'
]);
// Resultado en el log:
// [2024-01-15 10:30:45] [ERROR] Pago fallido {"usuario_id":123,"monto":99.99,"codigo":"CARD_DECLINED"}
Manejador para APIs
Para APIs, los errores deben retornarse como JSON, no HTML:
<?php
declare(strict_types=1);
class ApiErrorHandler
{
private bool $debug;
public function __construct(bool $debug = false)
{
$this->debug = $debug;
}
public function handle(Throwable $e): void
{
// Determinar código HTTP según el tipo de excepción
$statusCode = $this->getStatusCode($e);
// Log del error
error_log(sprintf(
"[API Error] %s: %s en %s:%d",
get_class($e),
$e->getMessage(),
$e->getFile(),
$e->getLine()
));
// Respuesta JSON
http_response_code($statusCode);
header('Content-Type: application/json');
$respuesta = [
'error' => true,
'message' => $this->getMensajePublico($e),
];
if ($this->debug) {
$respuesta['debug'] = [
'exception' => get_class($e),
'file' => $e->getFile(),
'line' => $e->getLine(),
'trace' => explode("\n", $e->getTraceAsString())
];
}
echo json_encode($respuesta, JSON_PRETTY_PRINT);
exit(1);
}
private function getStatusCode(Throwable $e): int
{
return match (true) {
$e instanceof InvalidArgumentException => 400,
$e instanceof RuntimeException => 500,
method_exists($e, 'getStatusCode') => $e->getStatusCode(),
default => 500
};
}
private function getMensajePublico(Throwable $e): string
{
// En producción, solo mostrar mensajes "seguros"
if (!$this->debug && $this->getStatusCode($e) === 500) {
return 'Ha ocurrido un error interno';
}
return $e->getMessage();
}
}
// Registrar el handler
$apiHandler = new ApiErrorHandler(debug: getenv('APP_ENV') !== 'production');
set_exception_handler([$apiHandler, 'handle']);
Nunca expongas stack traces, rutas de archivos ni detalles internos en producción. Esta información puede ayudar a atacantes a explotar vulnerabilidades.
Ejercicios
Ejercicio 1: Handler con niveles de log
Crea un ErrorHandler que registre
errores en diferentes archivos según su
gravedad: critical.log para
errores fatales,
error.log para excepciones,
warning.log para warnings.
Ver solución
<?php
declare(strict_types=1);
class MultiLogErrorHandler
{
private string $logDir;
public function __construct(string $logDir)
{
$this->logDir = rtrim($logDir, '/');
}
public function register(): void
{
set_error_handler([$this, 'handleError']);
set_exception_handler([$this, 'handleException']);
register_shutdown_function([$this, 'handleShutdown']);
}
public function handleError(int $nivel, string $mensaje, string $archivo, int $linea): bool
{
if (!(error_reporting() & $nivel)) {
return false;
}
$esWarning = in_array($nivel, [E_WARNING, E_USER_WARNING, E_NOTICE, E_USER_NOTICE]);
$logFile = $esWarning ? 'warning.log' : 'error.log';
$this->log($logFile, $mensaje, $archivo, $linea);
return true;
}
public function handleException(Throwable $e): void
{
$this->log('error.log', $e->getMessage(), $e->getFile(), $e->getLine());
}
public function handleShutdown(): void
{
$error = error_get_last();
if ($error && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR])) {
$this->log('critical.log', $error['message'], $error['file'], $error['line']);
}
}
private function log(string $archivo, string $mensaje, string $file, int $linea): void
{
$entrada = sprintf(
"[%s] %s en %s:%d\n",
date('Y-m-d H:i:s'),
$mensaje,
$file,
$linea
);
file_put_contents($this->logDir . '/' . $archivo, $entrada, FILE_APPEND);
}
}
// Uso
$handler = new MultiLogErrorHandler('/var/log/app');
$handler->register();
Ejercicio 2: Notificación de errores críticos
Extiende el ErrorHandler para que cuando
ocurra un error fatal, además de
registrarlo, envíe una notificación
(simula el envío guardando en un archivo
notifications.log).
Ver solución
<?php
declare(strict_types=1);
class NotifyingErrorHandler
{
private string $logPath;
private string $notifyPath;
public function __construct(string $logPath, string $notifyPath)
{
$this->logPath = $logPath;
$this->notifyPath = $notifyPath;
}
public function register(): void
{
set_exception_handler([$this, 'handleException']);
register_shutdown_function([$this, 'handleShutdown']);
}
public function handleException(Throwable $e): void
{
$this->log($e->getMessage(), $e->getFile(), $e->getLine());
// Notificar errores críticos
if ($e instanceof Error) {
$this->notificar($e);
}
}
public function handleShutdown(): void
{
$error = error_get_last();
if ($error && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR])) {
$this->log($error['message'], $error['file'], $error['line']);
$this->notificar(new ErrorException(
$error['message'], 0, $error['type'], $error['file'], $error['line']
));
}
}
private function log(string $mensaje, string $archivo, int $linea): void
{
$entrada = sprintf("[%s] %s en %s:%d\n", date('Y-m-d H:i:s'), $mensaje, $archivo, $linea);
file_put_contents($this->logPath, $entrada, FILE_APPEND);
}
private function notificar(Throwable $e): void
{
$notificacion = sprintf(
"[%s] ERROR CRÍTICO\nMensaje: %s\nArchivo: %s:%d\n---\n",
date('Y-m-d H:i:s'),
$e->getMessage(),
$e->getFile(),
$e->getLine()
);
file_put_contents($this->notifyPath, $notificacion, FILE_APPEND);
}
}
// Uso
$handler = new NotifyingErrorHandler('/var/log/app.log', '/var/log/notifications.log');
$handler->register();
Ejercicio 3: Rate limiting de logs
Implementa un sistema que evite registrar el mismo error más de N veces por minuto para evitar llenar los logs. Usa un archivo temporal para trackear los errores recientes.
Ver solución
<?php
declare(strict_types=1);
class RateLimitedLogger
{
private string $logPath;
private string $trackPath;
private int $maxPorMinuto;
public function __construct(string $logPath, int $maxPorMinuto = 5)
{
$this->logPath = $logPath;
$this->trackPath = sys_get_temp_dir() . '/error_tracker.json';
$this->maxPorMinuto = $maxPorMinuto;
}
public function log(Throwable $e): void
{
$hash = md5($e->getMessage() . $e->getFile() . $e->getLine());
if (!$this->puedeRegistrar($hash)) {
return; // Rate limited, ignorar
}
$entrada = sprintf(
"[%s] %s: %s en %s:%d\n",
date('Y-m-d H:i:s'),
get_class($e),
$e->getMessage(),
$e->getFile(),
$e->getLine()
);
file_put_contents($this->logPath, $entrada, FILE_APPEND);
}
private function puedeRegistrar(string $hash): bool
{
$tracker = $this->cargarTracker();
$ahora = time();
$hace1min = $ahora - 60;
// Limpiar entradas antiguas
foreach ($tracker as $key => $datos) {
$tracker[$key] = array_filter($datos, fn($t) => $t > $hace1min);
if (empty($tracker[$key])) {
unset($tracker[$key]);
}
}
// Verificar límite
$ocurrencias = $tracker[$hash] ?? [];
if (count($ocurrencias) >= $this->maxPorMinuto) {
$this->guardarTracker($tracker);
return false;
}
// Registrar ocurrencia
$tracker[$hash][] = $ahora;
$this->guardarTracker($tracker);
return true;
}
private function cargarTracker(): array
{
if (!file_exists($this->trackPath)) {
return [];
}
return json_decode(file_get_contents($this->trackPath), true) ?? [];
}
private function guardarTracker(array $tracker): void
{
file_put_contents($this->trackPath, json_encode($tracker));
}
}
// Uso
$logger = new RateLimitedLogger('/var/log/app.log', 3);
set_exception_handler(function (Throwable $e) use ($logger): void {
$logger->log($e);
echo "Error registrado (si no excede el límite)";
});
¿Has encontrado un error o tienes sugerencias para esta lección?
Enviar feedback¿Te está gustando el curso?
Tenemos cursos premium con proyectos reales y soporte.
Descubrir cursos premium