Lección 50 de 75 12 min de lectura

Excepciones en PHP

Las excepciones son el mecanismo moderno para manejar errores en PHP. Permiten separar el código que puede fallar del código que maneja esos fallos, haciendo tu aplicación más robusta y mantenible.

¿Qué es una excepción?

Una excepción es un objeto que representa un error o situación excepcional. Cuando algo sale mal, "lanzas" una excepción. En algún punto del código, debes "capturarla" y decidir qué hacer.

PHP
<?php

declare(strict_types=1);

function dividir(float $a, float $b): float
{
    if ($b === 0.0) {
        throw new InvalidArgumentException('No se puede dividir por cero');
    }
    return $a / $b;
}

// Uso con try-catch
try {
    $resultado = dividir(10, 0);
    echo "Resultado: $resultado";
} catch (InvalidArgumentException $e) {
    echo 'Error: ' . $e->getMessage();
    // Error: No se puede dividir por cero
}

Anatomía de try-catch-finally

PHP
<?php

declare(strict_types=1);

try {
    // Código que puede lanzar excepciones
    $archivo = fopen('datos.txt', 'r');
    if ($archivo === false) {
        throw new RuntimeException('No se pudo abrir el archivo');
    }

    $contenido = fread($archivo, filesize('datos.txt'));
    // Procesar contenido...

} catch (RuntimeException $e) {
    // Se ejecuta si ocurre RuntimeException
    echo 'Error de ejecución: ' . $e->getMessage();

} catch (Exception $e) {
    // Se ejecuta para cualquier otra Exception
    echo 'Error general: ' . $e->getMessage();

} finally {
    // SIEMPRE se ejecuta, haya error o no
    // Ideal para liberar recursos
    if (isset($archivo) && $archivo !== false) {
        fclose($archivo);
    }
    echo 'Limpieza completada';
}
El bloque finally

finally se ejecuta siempre: si hay excepción, si no la hay, e incluso si hay un return dentro del try o catch.

Lanzar excepciones con throw

Usa throw para lanzar una excepción cuando detectas una situación que no puedes manejar:

PHP
<?php

declare(strict_types=1);

class Usuario
{
    private string $email;
    private int $edad;

    public function __construct(string $email, int $edad)
    {
        // Validar email
        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            throw new InvalidArgumentException("Email inválido: $email");
        }

        // Validar edad
        if ($edad < 0 || $edad > 150) {
            throw new InvalidArgumentException("Edad inválida: $edad");
        }

        $this->email = $email;
        $this->edad = $edad;
    }
}

// Uso
try {
    $usuario = new Usuario('correo-invalido', 25);
} catch (InvalidArgumentException $e) {
    echo $e->getMessage(); // Email inválido: correo-invalido
}

try {
    $usuario = new Usuario('ana@example.com', -5);
} catch (InvalidArgumentException $e) {
    echo $e->getMessage(); // Edad inválida: -5
}

Capturar múltiples excepciones

Puedes capturar diferentes tipos de excepciones y manejar cada una de forma específica:

PHP
<?php

declare(strict_types=1);

function procesarArchivo(string $ruta): array
{
    if (!file_exists($ruta)) {
        throw new RuntimeException("Archivo no encontrado: $ruta");
    }

    $contenido = file_get_contents($ruta);
    if ($contenido === false) {
        throw new RuntimeException("No se pudo leer: $ruta");
    }

    $datos = json_decode($contenido, true);
    if (json_last_error() !== JSON_ERROR_NONE) {
        throw new InvalidArgumentException('JSON inválido: ' . json_last_error_msg());
    }

    return $datos;
}

try {
    $datos = procesarArchivo('config.json');
    print_r($datos);

} catch (RuntimeException $e) {
    // Problemas con el archivo
    echo 'Error de archivo: ' . $e->getMessage();

} catch (InvalidArgumentException $e) {
    // Problemas con el formato
    echo 'Error de formato: ' . $e->getMessage();
}

// PHP 8+: Capturar múltiples tipos en un solo catch
try {
    $datos = procesarArchivo('config.json');
} catch (RuntimeException | InvalidArgumentException $e) {
    echo 'Error: ' . $e->getMessage();
}

Información de la excepción

Las excepciones contienen información útil para depuración:

PHP
<?php

declare(strict_types=1);

try {
    throw new Exception('Algo salió mal', 500);
} catch (Exception $e) {
    // Mensaje de error
    echo $e->getMessage();   // 'Algo salió mal'

    // Código de error (opcional, lo defines tú)
    echo $e->getCode();      // 500

    // Archivo donde ocurrió
    echo $e->getFile();      // /ruta/al/archivo.php

    // Línea donde ocurrió
    echo $e->getLine();      // 5

    // Stack trace como array
    print_r($e->getTrace());

    // Stack trace como string (útil para logs)
    echo $e->getTraceAsString();

    // Representación completa
    echo (string) $e;
}

Excepciones anidadas (cause)

Puedes encadenar excepciones para mantener el contexto del error original:

PHP
<?php

declare(strict_types=1);

function obtenerConfiguracion(string $archivo): array
{
    try {
        $contenido = file_get_contents($archivo);
        if ($contenido === false) {
            throw new RuntimeException("No se pudo leer el archivo");
        }
        return json_decode($contenido, true, flags: JSON_THROW_ON_ERROR);

    } catch (JsonException $e) {
        // Relanzar con contexto adicional, manteniendo la original
        throw new RuntimeException(
            "Error al parsear configuración: $archivo",
            0,
            $e  // Excepción original como "cause"
        );
    }
}

try {
    $config = obtenerConfiguracion('config.json');
} catch (RuntimeException $e) {
    echo $e->getMessage();
    // Error al parsear configuración: config.json

    // Acceder a la excepción original
    $causa = $e->getPrevious();
    if ($causa !== null) {
        echo ' | Causa: ' . $causa->getMessage();
        // Causa: Syntax error
    }
}

Excepciones SPL comunes

PHP incluye excepciones predefinidas en la SPL (Standard PHP Library) para situaciones comunes:

PHP
<?php

declare(strict_types=1);

// InvalidArgumentException: argumento inválido
function setEdad(int $edad): void
{
    if ($edad < 0) {
        throw new InvalidArgumentException('La edad no puede ser negativa');
    }
}

// OutOfRangeException: índice fuera de rango
function obtenerElemento(array $items, int $indice): mixed
{
    if ($indice < 0 || $indice >= count($items)) {
        throw new OutOfRangeException("Índice $indice fuera de rango");
    }
    return $items[$indice];
}

// LengthException: longitud inválida
function crearPassword(string $password): string
{
    if (strlen($password) < 8) {
        throw new LengthException('La contraseña debe tener al menos 8 caracteres');
    }
    return password_hash($password, PASSWORD_DEFAULT);
}

// RuntimeException: error en tiempo de ejecución
function conectarBaseDatos(string $dsn): PDO
{
    try {
        return new PDO($dsn);
    } catch (PDOException $e) {
        throw new RuntimeException('No se pudo conectar a la base de datos', 0, $e);
    }
}

// LogicException: error de lógica en el código
class Calculadora
{
    private bool $inicializada = false;

    public function inicializar(): void
    {
        $this->inicializada = true;
    }

    public function calcular(int $valor): int
    {
        if (!$this->inicializada) {
            throw new LogicException('Debes llamar a inicializar() primero');
        }
        return $valor * 2;
    }
}

Relanzar excepciones

A veces necesitas capturar una excepción, hacer algo (como registrarla), y luego relanzarla:

PHP
<?php

declare(strict_types=1);

function procesarPago(float $monto): bool
{
    try {
        // Intentar procesar el pago
        return realizarTransaccion($monto);

    } catch (Exception $e) {
        // Registrar el error
        error_log('Error en pago: ' . $e->getMessage());

        // Relanzar para que el código superior lo maneje
        throw $e;
    }
}

// También puedes transformar la excepción
function obtenerUsuario(int $id): array
{
    try {
        return consultarBaseDatos($id);

    } catch (PDOException $e) {
        // Convertir excepción técnica en una más descriptiva
        throw new RuntimeException(
            "No se pudo obtener el usuario con ID: $id",
            0,
            $e
        );
    }
}

Ejemplo práctico: Validador con excepciones

PHP
<?php

declare(strict_types=1);

class Validador
{
    public static function email(string $email): string
    {
        $email = trim($email);
        if ($email === '') {
            throw new InvalidArgumentException('El email es requerido');
        }
        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            throw new InvalidArgumentException('El email no es válido');
        }
        return $email;
    }

    public static function password(string $password): string
    {
        if (strlen($password) < 8) {
            throw new LengthException('La contraseña debe tener al menos 8 caracteres');
        }
        if (!preg_match('/[A-Z]/', $password)) {
            throw new InvalidArgumentException('Debe contener al menos una mayúscula');
        }
        if (!preg_match('/[0-9]/', $password)) {
            throw new InvalidArgumentException('Debe contener al menos un número');
        }
        return $password;
    }

    public static function edad(int $edad): int
    {
        if ($edad < 18) {
            throw new InvalidArgumentException('Debes ser mayor de edad');
        }
        if ($edad > 120) {
            throw new InvalidArgumentException('Edad no válida');
        }
        return $edad;
    }
}

// Uso
function registrarUsuario(array $datos): array
{
    $errores = [];
    $usuario = [];

    try {
        $usuario['email'] = Validador::email($datos['email'] ?? '');
    } catch (InvalidArgumentException $e) {
        $errores['email'] = $e->getMessage();
    }

    try {
        $usuario['password'] = Validador::password($datos['password'] ?? '');
    } catch (InvalidArgumentException | LengthException $e) {
        $errores['password'] = $e->getMessage();
    }

    try {
        $usuario['edad'] = Validador::edad((int) ($datos['edad'] ?? 0));
    } catch (InvalidArgumentException $e) {
        $errores['edad'] = $e->getMessage();
    }

    if (!empty($errores)) {
        throw new RuntimeException('Errores de validación: ' . json_encode($errores));
    }

    return $usuario;
}

// Probar
try {
    $usuario = registrarUsuario([
        'email' => 'test@example.com',
        'password' => 'Segura123',
        'edad' => 25
    ]);
    echo 'Usuario registrado correctamente';
} catch (RuntimeException $e) {
    echo $e->getMessage();
}

Ejercicios

Ejercicio 1: Función con validación

Crea una función calcularDescuento(float $precio, int $porcentaje) que lance InvalidArgumentException si el precio es negativo o el porcentaje no está entre 0 y 100. Retorna el precio con descuento aplicado.

Ver solución
<?php

declare(strict_types=1);

function calcularDescuento(float $precio, int $porcentaje): float
{
    if ($precio < 0) {
        throw new InvalidArgumentException('El precio no puede ser negativo');
    }

    if ($porcentaje < 0 || $porcentaje > 100) {
        throw new InvalidArgumentException(
            'El porcentaje debe estar entre 0 y 100'
        );
    }

    return $precio - ($precio * $porcentaje / 100);
}

// Pruebas
try {
    echo calcularDescuento(100, 20) . "\n"; // 80
    echo calcularDescuento(50, 10) . "\n";  // 45
    echo calcularDescuento(-10, 20);        // Lanza excepción
} catch (InvalidArgumentException $e) {
    echo "Error: " . $e->getMessage();
}

Ejercicio 2: Lector de JSON seguro

Crea una función leerJsonSeguro(string $ruta) que lea un archivo JSON. Debe lanzar RuntimeException si el archivo no existe o no se puede leer, e InvalidArgumentException si el JSON es inválido. Usa excepciones anidadas para preservar la causa original.

Ver solución
<?php

declare(strict_types=1);

function leerJsonSeguro(string $ruta): array
{
    if (!file_exists($ruta)) {
        throw new RuntimeException("Archivo no encontrado: $ruta");
    }

    $contenido = file_get_contents($ruta);
    if ($contenido === false) {
        throw new RuntimeException("No se pudo leer el archivo: $ruta");
    }

    try {
        $datos = json_decode($contenido, true, flags: JSON_THROW_ON_ERROR);
    } catch (JsonException $e) {
        throw new InvalidArgumentException(
            "JSON inválido en $ruta",
            0,
            $e  // Excepción original como causa
        );
    }

    return $datos;
}

// Uso
try {
    $config = leerJsonSeguro('config.json');
    print_r($config);
} catch (RuntimeException $e) {
    echo "Error de archivo: " . $e->getMessage();
} catch (InvalidArgumentException $e) {
    echo "Error de formato: " . $e->getMessage();
    if ($e->getPrevious()) {
        echo "\nCausa: " . $e->getPrevious()->getMessage();
    }
}

Ejercicio 3: Clase Cuenta bancaria

Crea una clase CuentaBancaria con métodos depositar() y retirar(). Lanza excepciones apropiadas: InvalidArgumentException para montos negativos, RuntimeException si se intenta retirar más del saldo disponible.

Ver solución
<?php

declare(strict_types=1);

class CuentaBancaria
{
    private float $saldo;

    public function __construct(float $saldoInicial = 0)
    {
        if ($saldoInicial < 0) {
            throw new InvalidArgumentException('El saldo inicial no puede ser negativo');
        }
        $this->saldo = $saldoInicial;
    }

    public function depositar(float $monto): void
    {
        if ($monto <= 0) {
            throw new InvalidArgumentException('El monto debe ser positivo');
        }
        $this->saldo += $monto;
    }

    public function retirar(float $monto): void
    {
        if ($monto <= 0) {
            throw new InvalidArgumentException('El monto debe ser positivo');
        }
        if ($monto > $this->saldo) {
            throw new RuntimeException(
                "Saldo insuficiente. Disponible: {$this->saldo}, solicitado: $monto"
            );
        }
        $this->saldo -= $monto;
    }

    public function getSaldo(): float
    {
        return $this->saldo;
    }
}

// Pruebas
try {
    $cuenta = new CuentaBancaria(100);
    $cuenta->depositar(50);
    echo "Saldo: " . $cuenta->getSaldo() . "\n"; // 150

    $cuenta->retirar(200); // Lanza RuntimeException
} catch (InvalidArgumentException $e) {
    echo "Error de validación: " . $e->getMessage();
} catch (RuntimeException $e) {
    echo "Error de operación: " . $e->getMessage();
}

¿Te está gustando el curso?

Tenemos cursos premium con proyectos reales y soporte.

Descubrir cursos premium