Tema
Errores y excepciones
Todo programa falla en algún momento: la base de datos se cae, el usuario envía datos raros, un archivo no existe. La diferencia entre un sitio amateur y uno profesional es cómo maneja esos fallos: mostrar un mensaje amable al usuario, registrar el detalle para el desarrollador y nunca revelar información sensible.
Concepto
En PHP hay dos familias de problemas:
- Errores clásicos (warnings, notices, deprecated): avisos que PHP muestra pero que normalmente no detienen el script. Ejemplo: leer una variable que no existe.
- Excepciones y
Error: objetos que se "lanzan" (throw) cuando algo sale mal y que detienen la ejecución si nadie los "atrapa" (catch).
Una analogía: una excepción es como una alarma de incendio. Cuando suena (throw), todo se detiene y la alarma sube piso por piso (por las funciones que llamaron) hasta que alguien la atiende (catch). Si nadie la atiende, el edificio entero se evacúa (error fatal).
Jerarquía en PHP 8
txt
Throwable (interfaz)
├── Error ← errores internos del motor
│ ├── TypeError ← tipo incorrecto en un parámetro
│ ├── ValueError ← valor inválido (ej. array_chunk con tamaño 0)
│ ├── ArithmeticError
│ │ └── DivisionByZeroError
│ └── ...
└── Exception ← excepciones de tu aplicación y librerías
├── RuntimeException
├── InvalidArgumentException (hija de LogicException)
├── PDOException
├── JsonException
└── ...Configurar la visualización de errores
En desarrollo quieres ver todo; en producción, nada en pantalla y todo en un log.
ini
; php.ini en DESARROLLO
display_errors = On
error_reporting = E_ALL
; php.ini en PRODUCCIÓN
display_errors = Off
log_errors = On
error_log = /home/usuario/logs/php-errores.log
error_reporting = E_ALLTambién puedes hacerlo desde el código:
php
<?php
// config.php
$esProduccion = false; // cámbialo según el entorno
error_reporting(E_ALL);
ini_set('display_errors', $esProduccion ? '0' : '1');
ini_set('log_errors', '1');
ini_set('error_log', __DIR__ . '/logs/errores.log');Nunca en producción
display_errors = On en un servidor público puede mostrar rutas internas, consultas SQL e incluso contraseñas de la base de datos en el mensaje de error. Un regalo para los atacantes.
try, catch, finally
php
<?php
declare(strict_types=1);
function dividir(int $a, int $b): float
{
if ($b === 0) {
// Lanzamos una excepción con un mensaje claro
throw new InvalidArgumentException('No se puede dividir entre cero.');
}
return $a / $b;
}
try {
echo dividir(10, 2), PHP_EOL; // 5
echo dividir(10, 0), PHP_EOL; // lanza la excepción, la línea siguiente no se ejecuta
echo 'Esto nunca se imprime';
} catch (InvalidArgumentException $e) {
// Atrapamos SOLO ese tipo de excepción
echo 'Error: ' . $e->getMessage(), PHP_EOL;
} finally {
// Se ejecuta SIEMPRE, haya o no excepción
echo 'Operación terminada', PHP_EOL;
}Salida:
txt
5
Error: No se puede dividir entre cero.
Operación terminadaMétodos útiles de una excepción
php
<?php
try {
throw new RuntimeException('Falló el pago', 402);
} catch (RuntimeException $e) {
echo $e->getMessage(); // Falló el pago
echo $e->getCode(); // 402
echo $e->getFile(); // /ruta/al/archivo.php
echo $e->getLine(); // 3
echo $e->getTraceAsString(); // pila de llamadas
$anterior = $e->getPrevious(); // excepción original (si se encadenó)
}Varios catch y multi-catch
php
<?php
try {
$datos = json_decode($entrada, true, flags: JSON_THROW_ON_ERROR);
procesarPedido($datos);
} catch (JsonException $e) {
echo 'El JSON enviado no es válido.';
} catch (InvalidArgumentException | DomainException $e) {
// Multi-catch: el mismo bloque para dos tipos
echo 'Datos del pedido incorrectos: ' . $e->getMessage();
} catch (Throwable $e) {
// Atrapa TODO lo demás (incluidos Error y TypeError). Ponlo al final.
error_log($e);
echo 'Ocurrió un error inesperado.';
}Buena práctica
Ordena los catch del más específico al más general. Si pones catch (Exception $e) primero, los siguientes nunca se ejecutarán.
Excepciones personalizadas
Crear tus propias clases de excepción hace el código más legible y te permite reaccionar distinto a cada problema.
php
<?php
declare(strict_types=1);
// Excepción base de tu aplicación
class AppException extends Exception {}
// Excepciones específicas
class ValidacionException extends AppException
{
// Guardamos los errores por campo
public function __construct(private array $errores)
{
parent::__construct('Los datos no son válidos.');
}
public function getErrores(): array
{
return $this->errores;
}
}
class StockInsuficienteException extends AppException {}
function registrarUsuario(array $datos): void
{
$errores = [];
if (empty($datos['nombre'])) {
$errores['nombre'] = 'El nombre es obligatorio.';
}
if (!filter_var($datos['email'] ?? '', FILTER_VALIDATE_EMAIL)) {
$errores['email'] = 'El email no es válido.';
}
if ($errores) {
throw new ValidacionException($errores);
}
// ... guardar en la base de datos
}
try {
registrarUsuario(['nombre' => '', 'email' => 'no-es-email']);
} catch (ValidacionException $e) {
foreach ($e->getErrores() as $campo => $mensaje) {
echo "$campo: $mensaje" . PHP_EOL;
}
}Encadenar excepciones
Cuando atrapas una excepción técnica y lanzas otra más comprensible, conserva la original con el tercer parámetro:
php
<?php
function obtenerProducto(PDO $pdo, int $id): array
{
try {
$stmt = $pdo->prepare('SELECT * FROM productos WHERE id = ?');
$stmt->execute([$id]);
return $stmt->fetch() ?: throw new RuntimeException("Producto $id no existe");
} catch (PDOException $e) {
// El usuario ve el mensaje genérico; el log conserva la causa real
throw new RuntimeException('No se pudo consultar el producto.', 0, $e);
}
}Fíjate en ?: throw ...: desde PHP 8, throw es una expresión y puede usarse dentro de ??, ?:, match y funciones flecha.
Convertir warnings en excepciones
Muchas funciones antiguas de PHP sólo lanzan un warning y devuelven false (por ejemplo file_get_contents). Con set_error_handler puedes convertirlos en excepciones y manejar todo igual:
php
<?php
set_error_handler(function (int $nivel, string $mensaje, string $archivo, int $linea): bool {
// Respeta el operador @ y la configuración de error_reporting
if (!(error_reporting() & $nivel)) {
return false;
}
throw new ErrorException($mensaje, 0, $nivel, $archivo, $linea);
});
try {
$contenido = file_get_contents('no-existe.txt');
} catch (ErrorException $e) {
echo 'No se pudo leer el archivo.';
}Manejador global: la red de seguridad
Toda aplicación profesional tiene un manejador para las excepciones que nadie atrapó. Muestra una página amable y guarda el detalle en el log.
php
<?php
// bootstrap.php — inclúyelo al inicio de cada página
declare(strict_types=1);
const EN_PRODUCCION = false;
error_reporting(E_ALL);
ini_set('display_errors', EN_PRODUCCION ? '0' : '1');
ini_set('log_errors', '1');
ini_set('error_log', __DIR__ . '/logs/app.log');
set_exception_handler(function (Throwable $e): void {
// 1. Registrar todo el detalle para el desarrollador
error_log(sprintf(
"[%s] %s: %s en %s:%d\n%s",
date('Y-m-d H:i:s'),
$e::class,
$e->getMessage(),
$e->getFile(),
$e->getLine(),
$e->getTraceAsString()
));
// 2. Responder al usuario
http_response_code(500);
if (EN_PRODUCCION) {
echo '<h1>Algo salió mal</h1><p>Ya estamos trabajando en ello. Intenta más tarde.</p>';
} else {
echo '<h1>' . htmlspecialchars($e::class) . '</h1>';
echo '<p>' . htmlspecialchars($e->getMessage()) . '</p>';
echo '<pre>' . htmlspecialchars($e->getTraceAsString()) . '</pre>';
}
});Buena práctica
Crea la carpeta logs/ fuera de la carpeta pública o protégela con un .htaccess (Require all denied) para que nadie pueda descargar tus logs.
Errores comunes de PHP 8 y qué significan
| Mensaje | Causa típica |
|---|---|
Warning: Undefined variable $x | Usas una variable que no definiste |
Warning: Undefined array key "email" | Lees $_POST['email'] sin comprobar si existe. Usa ?? |
TypeError: Argument #1 must be of type int, string given | Con strict_types=1, pasas un tipo incorrecto |
Error: Call to undefined function | Falta un require o una extensión de PHP |
Error: Call to a member function x() on null | La variable que creías objeto es null |
Fatal error: Allowed memory size exhausted | Bucle infinito o carga demasiados datos en memoria |
Parse error: syntax error, unexpected ... | Falta ;, una llave o un paréntesis (revisa la línea anterior) |
Error común
Silenciar errores con @ (@file_get_contents(...)) o con un catch vacío. El problema sigue ahí, sólo que ahora no te enteras. Si atrapas una excepción, haz algo con ella: regístrala, informa al usuario o relánzala.
php
// MAL
try { enviarCorreo(); } catch (Exception $e) { }
// BIEN
try { enviarCorreo(); } catch (Exception $e) {
error_log('Fallo al enviar correo: ' . $e->getMessage());
$aviso = 'Tu pedido se guardó, pero no pudimos enviarte el correo de confirmación.';
}Ejercicios
- Crea una función
leerConfig(string $ruta): arrayque lea un archivo JSON. Debe lanzarRuntimeExceptionsi el archivo no existe yJsonExceptionsi el contenido está mal formado. - Crea una excepción
SaldoInsuficienteExceptiony una clase sencillaCuentacon un métodoretirar(float $monto)que la lance si no hay saldo. Atrápala y muestra el saldo disponible. - Escribe un
bootstrap.phpque convierta warnings en excepciones y tenga un manejador global que escriba enlogs/app.log. Provoca un error para probarlo.
Ver solución
php
<?php
declare(strict_types=1);
// 1
function leerConfig(string $ruta): array
{
if (!is_file($ruta)) {
throw new RuntimeException("No existe el archivo de configuración: $ruta");
}
$texto = file_get_contents($ruta);
// JSON_THROW_ON_ERROR hace que json_decode lance JsonException
return json_decode($texto, true, 512, JSON_THROW_ON_ERROR);
}
try {
$config = leerConfig(__DIR__ . '/config.json');
} catch (JsonException $e) {
echo 'El archivo de configuración tiene un JSON inválido: ' . $e->getMessage();
} catch (RuntimeException $e) {
echo $e->getMessage();
}
// 2
class SaldoInsuficienteException extends Exception
{
public function __construct(public readonly float $disponible)
{
parent::__construct('Saldo insuficiente.');
}
}
class Cuenta
{
public function __construct(private float $saldo = 0) {}
public function retirar(float $monto): void
{
if ($monto > $this->saldo) {
throw new SaldoInsuficienteException($this->saldo);
}
$this->saldo -= $monto;
}
}
$cuenta = new Cuenta(100);
try {
$cuenta->retirar(250);
} catch (SaldoInsuficienteException $e) {
echo $e->getMessage() . ' Disponible: $' . number_format($e->disponible, 2);
}php
<?php
// 3: bootstrap.php
declare(strict_types=1);
if (!is_dir(__DIR__ . '/logs')) {
mkdir(__DIR__ . '/logs', 0755, true);
}
ini_set('log_errors', '1');
ini_set('error_log', __DIR__ . '/logs/app.log');
ini_set('display_errors', '0');
set_error_handler(function (int $n, string $m, string $f, int $l): bool {
if (!(error_reporting() & $n)) return false;
throw new ErrorException($m, 0, $n, $f, $l);
});
set_exception_handler(function (Throwable $e): void {
error_log($e::class . ': ' . $e->getMessage() . ' en ' . $e->getFile() . ':' . $e->getLine());
http_response_code(500);
echo 'Error interno. Revisa logs/app.log';
});
// Prueba: lee una clave que no existe -> warning -> ErrorException -> manejador global
$datos = [];
echo $datos['no_existe'];Resumen
- En desarrollo muestra todos los errores; en producción, ocúltalos y regístralos en un log.
try/catch/finallycontrolan las excepciones; ordena loscatchde específico a general.throwes una expresión en PHP 8: úsala con??,?:ymatch.- Crea excepciones propias que hereden de
Exceptionpara expresar errores de negocio. - Encadena excepciones con el parámetro
$previouspara no perder la causa original. set_error_handlerconvierte warnings en excepciones;set_exception_handleres tu red de seguridad global.