Skip to content

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:

  1. Errores clásicos (warnings, notices, deprecated): avisos que PHP muestra pero que normalmente no detienen el script. Ejemplo: leer una variable que no existe.
  2. 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_ALL

Tambié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 terminada

Mé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 ​

MensajeCausa típica
Warning: Undefined variable $xUsas 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 givenCon strict_types=1, pasas un tipo incorrecto
Error: Call to undefined functionFalta un require o una extensión de PHP
Error: Call to a member function x() on nullLa variable que creías objeto es null
Fatal error: Allowed memory size exhaustedBucle 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 ​

  1. Crea una función leerConfig(string $ruta): array que lea un archivo JSON. Debe lanzar RuntimeException si el archivo no existe y JsonException si el contenido está mal formado.
  2. Crea una excepción SaldoInsuficienteException y una clase sencilla Cuenta con un método retirar(float $monto) que la lance si no hay saldo. Atrápala y muestra el saldo disponible.
  3. Escribe un bootstrap.php que convierta warnings en excepciones y tenga un manejador global que escriba en logs/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 / finally controlan las excepciones; ordena los catch de específico a general.
  • throw es una expresión en PHP 8: úsala con ??, ?: y match.
  • Crea excepciones propias que hereden de Exception para expresar errores de negocio.
  • Encadena excepciones con el parámetro $previous para no perder la causa original.
  • set_error_handler convierte warnings en excepciones; set_exception_handler es tu red de seguridad global.

Academia Web — aprende, copia y construye.