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
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
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';
}
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
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
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
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
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
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
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
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();
}
¿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