Curso de PHP

Excepciones en PHP: try, catch, finally y throw

Por Víctor Peña · Publicado el

Excepciones en PHP

Hola, ¿cómo están? Continuando con el curso de PHP, hoy veremos las excepciones: la forma moderna de manejar situaciones donde algo sale mal.

¡Empecemos!

Qué es una excepción

Una excepción es un objeto que representa un error o una situación anormal durante la ejecución. Cuando se lanza, el flujo normal del programa se interrumpe y salta al lugar preparado para atenderla.

La diferencia con devolver false o null es importante: una excepción no se puede ignorar en silencio. Si nadie la atiende, el programa se detiene y te enteras.

El problema de no usarlas

<?php
function dividir(float $a, float $b): float
{
    if ($b === 0.0) {
        return 0;   // ¿resultado válido o error?
    }

    return $a / $b;
}

$resultado = dividir(10, 0);   // 0, pero ¿por qué?

Quien recibe ese 0 no sabe si es el resultado correcto o un error disfrazado. Y si olvida comprobarlo, el error se propaga silenciosamente.

Lanzar una excepción con throw

<?php
declare(strict_types=1);

function dividir(float $a, float $b): float
{
    if ($b === 0.0) {
        throw new InvalidArgumentException("No se puede dividir entre cero");
    }

    return $a / $b;
}

Ahora la función no puede devolver un resultado incorrecto: o divide bien, o lanza.

Capturar con try catch

<?php
try {
    $resultado = dividir(10, 0);
    echo $resultado;
} catch (InvalidArgumentException $e) {
    echo "Error: " . $e->getMessage();
}
  • try contiene el código que puede fallar.
  • catch recibe la excepción y decide qué hacer.

Si no ocurre nada, el catch se ignora por completo.

El bloque finally

finally se ejecuta siempre, haya error o no:

<?php
$archivo = fopen("datos.txt", "r");

try {
    // procesar el archivo
    throw new RuntimeException("Algo falló");
} catch (RuntimeException $e) {
    echo "Error: " . $e->getMessage();
} finally {
    fclose($archivo);   // se cierra pase lo que pase
}

Es el lugar para liberar recursos: cerrar archivos, cerrar conexiones, revertir una transacción.

Los métodos de una excepción

Todas las excepciones traen información útil:

<?php
try {
    throw new RuntimeException("Falló la conexión", 500);
} catch (RuntimeException $e) {
    echo $e->getMessage();   // Falló la conexión
    echo $e->getCode();      // 500
    echo $e->getFile();      // el archivo donde ocurrió
    echo $e->getLine();      // la línea
    echo $e->getTraceAsString();  // la pila de llamadas
}

getTraceAsString() es lo que te dice cómo se llegó hasta el error, y suele ser lo más valioso al depurar.

La jerarquía de excepciones

PHP organiza los errores en una jerarquía. Conocerla te permite capturar con la precisión adecuada:

Throwable
├── Error                      ← errores del propio PHP
│   ├── TypeError
│   ├── ArgumentCountError
│   └── DivisionByZeroError
└── Exception                  ← excepciones de la aplicación
    ├── RuntimeException
    │   ├── OutOfBoundsException
    │   └── UnexpectedValueException
    ├── LogicException
    │   ├── InvalidArgumentException
    │   └── DomainException
    └── JsonException

La distinción de fondo:

  • LogicException y sus hijas: errores del programador. No debieron ocurrir; se arreglan cambiando el código.
  • RuntimeException y sus hijas: errores que dependen de circunstancias. Un archivo que no existe, una conexión caída.

Error y Exception son ramas distintas. Por eso catch (Exception $e) no captura un TypeError. Si necesitas capturar cualquier cosa:

<?php
try {
    // ...
} catch (Throwable $e) {
    // atrapa absolutamente todo
}

Capturar varios tipos

El orden importa: de lo más específico a lo más general.

<?php
try {
    procesarPedido($datos);
} catch (InvalidArgumentException $e) {
    echo "Datos incorrectos: " . $e->getMessage();
} catch (RuntimeException $e) {
    echo "Error de ejecución: " . $e->getMessage();
} catch (Throwable $e) {
    echo "Error inesperado";
}

Si pusieras Throwable primero, capturaría todo y los demás nunca se ejecutarían.

También puedes agrupar tipos con |:

<?php
try {
    // ...
} catch (InvalidArgumentException | DomainException $e) {
    echo "Problema con los datos";
}

Crear excepciones personalizadas

Es una práctica muy recomendable: excepciones con nombre propio que describen problemas de tu dominio.

<?php
class SaldoInsuficienteException extends RuntimeException
{
    public function __construct(
        private float $saldoActual,
        private float $montoSolicitado,
    ) {
        parent::__construct(
            sprintf(
                "Saldo insuficiente: disponible %.2f, solicitado %.2f",
                $saldoActual,
                $montoSolicitado
            )
        );
    }

    public function getFaltante(): float
    {
        return $this->montoSolicitado - $this->saldoActual;
    }
}

Y al usarla:

<?php
try {
    $cuenta->retirar(5000);
} catch (SaldoInsuficienteException $e) {
    echo $e->getMessage();
    echo "Te faltan " . $e->getFaltante();
}

La ventaja es doble: el catch es específico —no atrapas errores que no te tocan— y la excepción puede llevar datos útiles además del mensaje.

Relanzar y encadenar

A veces capturas una excepción para añadir contexto y volver a lanzarla:

<?php
try {
    $this->conectar();
} catch (PDOException $e) {
    throw new RuntimeException(
        "No se pudo iniciar el reporte mensual",
        0,
        $e          // la excepción original
    );
}

El tercer parámetro conserva la excepción original, accesible con getPrevious(). Así el mensaje es comprensible para quien lo lee y no pierdes el detalle técnico.

Cuándo usar excepciones y cuándo no

Aquí hay un equilibrio que conviene tener claro.

Sí, lanza excepción cuando:

  • Los datos recibidos son inválidos.
  • Un recurso necesario no está disponible.
  • Se viola una regla del negocio.
  • El método no puede cumplir lo que promete.

No uses excepciones para:

  • Flujo normal del programa. Que una búsqueda no encuentre resultados no es excepcional: devuelve un array vacío.
  • Validación de formularios. Ahí quieres recoger todos los errores para mostrarlos juntos, no detenerte en el primero.

La regla: una excepción es para lo excepcional, no para lo previsible.

Un ejemplo práctico

<?php
declare(strict_types=1);

class PedidoNoEncontradoException extends RuntimeException {}
class PedidoYaPagadoException extends RuntimeException {}

class ServicioDePagos
{
    private array $pedidos = [
        1 => ['total' => 450.0, 'estado' => 'pendiente'],
        2 => ['total' => 300.0, 'estado' => 'pagado'],
    ];

    public function pagar(int $id, float $monto): string
    {
        if (!isset($this->pedidos[$id])) {
            throw new PedidoNoEncontradoException("No existe el pedido $id");
        }

        $pedido = $this->pedidos[$id];

        if ($pedido['estado'] === 'pagado') {
            throw new PedidoYaPagadoException("El pedido $id ya fue pagado");
        }

        if ($monto < $pedido['total']) {
            throw new InvalidArgumentException(
                sprintf("Monto insuficiente: faltan %.2f", $pedido['total'] - $monto)
            );
        }

        return "Pedido $id pagado correctamente";
    }
}

$servicio = new ServicioDePagos();

foreach ([[1, 450.0], [2, 300.0], [99, 100.0], [1, 50.0]] as [$id, $monto]) {
    try {
        echo $servicio->pagar($id, $monto) . "\n";
    } catch (PedidoNoEncontradoException $e) {
        echo "404: " . $e->getMessage() . "\n";
    } catch (PedidoYaPagadoException $e) {
        echo "Conflicto: " . $e->getMessage() . "\n";
    } catch (InvalidArgumentException $e) {
        echo "Datos: " . $e->getMessage() . "\n";
    }
}

Cada tipo de problema se atiende de forma distinta, y el código del método principal se lee limpio: valida, lanza y sigue.

Errores comunes

  • Capturar y no hacer nada. Un catch vacío esconde el problema y complica muchísimo la depuración.
  • Capturar Throwable de entrada. Atrapas incluso errores de programación que deberías ver.
  • Mostrar getMessage() al usuario final. Puede revelar rutas y detalles internos. Registra el detalle, muestra un mensaje genérico.
  • Usar excepciones para el flujo normal.
  • Poner el catch general antes que los específicos.

Para cerrar

Las excepciones convierten los errores silenciosos en errores visibles, y eso siempre es mejor. Lo importante: lanza cuando tu método no pueda cumplir lo que promete, captura solo lo que sepas atender, y usa finally para liberar recursos.

En la siguiente lección veremos los atributos y métodos estáticos.

Saludos y éxitos.

Norvic Software

Desarrollamos el software que tu empresa necesita

Somos una fábrica de software en Bolivia. Construimos sistemas a medida y aplicaciones móviles, y llevamos Inteligencia Artificial a las empresas que ya tienen un sistema funcionando.

Solicitar cotizaciónVer todos los servicios

Cotización sin costo · Respuesta directa por WhatsApp