libro / blog / errores-excepciones-php

// errores

Manejo de errores y excepciones en PHP: guía práctica

Gustavo Arias9 min de lectura

TL;DR En desarrollo, muestra todos los errores; en producción, ninguno: regístralos en un log y enseña al usuario una página genérica. Usa excepciones para las situaciones que no deberían ocurrir, captúralas solo donde puedas hacer algo útil y define un manejador global para lo que se escape.

Todo programa falla en algún momento: la base de datos no responde, una API tarda demasiado, el usuario envía algo inesperado. La diferencia entre una aplicación amateur y una profesional no es que falle menos, sino cómo falla: si muestra al visitante la ruta de tus archivos y tu contraseña de la base de datos, o una página amable mientras tú recibes el detalle en un log. En el libro PHP desde cero hasta experto el manejo de errores tiene su propio capítulo; esta es una guía práctica.

Errores vs excepciones

Desde PHP 7, la mayoría de los errores graves (llamar a una función que no existe, un tipo incorrecto con strict_types) son objetos Error, y tanto Error como Exception implementan la interfaz Throwable.

Configuración: desarrollo vs producción

Directiva de php.iniDesarrolloProducción
display_errorsOnOff
log_errorsOnOn
error_reportingE_ALLE_ALL
error_logOpcionalRuta a un archivo fuera de la carpeta pública

Fíjate en que error_reporting es E_ALL en ambos casos: en producción quieres registrar todo, solo que sin mostrarlo. Si no puedes editar php.ini en tu hosting, puedes fijarlo al inicio de la aplicación:

<?php
$esProduccion = getenv('APP_ENV') === 'production';

error_reporting(E_ALL);
ini_set('display_errors', $esProduccion ? '0' : '1');
ini_set('log_errors', '1');
ini_set('error_log', __DIR__ . '/../logs/php-errores.log');

try, catch y finally

<?php
function leerConfiguracion(string $ruta): array
{
    if (!is_readable($ruta)) {
        throw new RuntimeException("No se puede leer la configuración: $ruta");
    }
    return json_decode(file_get_contents($ruta), true, flags: JSON_THROW_ON_ERROR);
}

try {
    $config = leerConfiguracion(__DIR__ . '/config.json');
} catch (JsonException $e) {
    // el archivo existe pero el JSON está mal escrito
    error_log('config.json inválido: ' . $e->getMessage());
    $config = [];
} catch (RuntimeException $e) {
    error_log($e->getMessage());
    $config = [];
} finally {
    // se ejecuta siempre, haya excepción o no: cerrar archivos, liberar bloqueos...
}

Los bloques catch se evalúan en orden, así que pon primero las excepciones más específicas. JsonException va antes que RuntimeException porque es más concreta.

Excepciones propias

Crear tus propias clases de excepción permite distinguir los errores de tu dominio (“stock insuficiente”) de los técnicos (“la base de datos no responde”) y tratarlos de forma distinta:

<?php
declare(strict_types=1);

// Errores que se pueden mostrar al usuario tal cual
class ErrorDeValidacion extends Exception {}

class StockInsuficiente extends ErrorDeValidacion
{
    public function __construct(string $producto, int $disponible)
    {
        parent::__construct("Solo quedan $disponible unidades de $producto.");
    }
}

function reservar(string $producto, int $cantidad, int $stock): void
{
    if ($cantidad > $stock) {
        throw new StockInsuficiente($producto, $stock);
    }
    // ...
}

try {
    reservar('Teclado', 5, 3);
} catch (ErrorDeValidacion $e) {
    echo $e->getMessage();   // "Solo quedan 3 unidades de Teclado."
}

Un manejador global

Aunque captures lo previsible, siempre habrá algo que se escape. Un manejador global se asegura de que, pase lo que pase, el usuario ve una página digna y tú tienes el detalle en el log:

<?php
// Convierte avisos y advertencias en excepciones, para tratarlos igual
set_error_handler(function (int $nivel, string $mensaje, string $archivo, int $linea): bool {
    if (!(error_reporting() & $nivel)) {
        return false;   // error silenciado con @: dejar que PHP lo ignore
    }
    throw new ErrorException($mensaje, 0, $nivel, $archivo, $linea);
});

// Última red de seguridad: cualquier Throwable no capturado
set_exception_handler(function (Throwable $e): void {
    $id = bin2hex(random_bytes(4));   // referencia para cruzar con el log
    error_log(sprintf('[%s] %s en %s:%d', $id, $e->getMessage(), $e->getFile(), $e->getLine()));

    http_response_code(500);
    echo "Algo salió mal. Si el problema continúa, indícanos el código $id.";
});

Incluye este archivo al principio de tu aplicación, junto con la configuración de errores. El código de referencia permite que un usuario te reporte un fallo y lo encuentres en el log en segundos.

Antipatrones

Estos patrones encajan con el resto de la serie: la función de consumo de APIs y la de subida de imágenes lanzan excepciones precisamente para que puedas manejarlas así.

Aplicaciones que fallan con elegancia

El libro PHP desde cero hasta experto dedica un capítulo al manejo de errores, clave para llevar tus proyectos prácticos a producción.

Comprar en Amazon