Skip to content

3. Manejo de errores

Temas del programa: 3.1.3

Errores y situaciones no esperadas

SituaciónEjemploQuién la detectaRespuesta
Datos inválidosFalta fecha_finForm Request422
Regla de negocioEquipo ya reservadoService409
Sin identidadToken vencidoauth:sanctum401
Sin permisoPréstamo ajenoPolicy403
No existePréstamo 999Laravel404
No esperadaPostgreSQL no respondeNadie la planeó500 genérico

Las cinco primeras son esperadas se diseñan · la última se controla

Esperada

Parte del dominio · tiene nombre propio · el usuario puede corregirla · se registra como info

Inesperada

Falla técnica · el usuario no puede hacer nada · se registra como error con todo el detalle

class ConflictoDeReservaException extends RuntimeException
{
public function __construct(public readonly ?int $equipoId = null)
{
parent::__construct('El equipo ya está reservado en ese horario.');
}
}

Una excepción de dominio dice qué pasó y lleva los datos para diagnosticarlo

// PrestamoService
public function crear(array $datos): Prestamo
{
if ($this->repositorio->existeConflicto($datos)) {
throw new ConflictoDeReservaException((int) $datos['equipo_id']);
}
return $this->repositorio->crear($datos);
}

El Service no sabe si lo llamó la web o la API · no devuelve JSON ni códigos HTTP

bootstrap/app.php
$exceptions->render(function (ConflictoDeReservaException $e, Request $request) use ($error) {
if ($request->is('api/*')) {
Log::info('Reserva rechazada por conflicto de horario', [
'equipo_id' => $e->equipoId,
'user_id' => $request->user()?->id,
]);
return $error('CONFLICTO_RESERVA', $e->getMessage(), [], 409);
}
});

STD-BE-E02 · un render por excepción conocida · lo demás lo maneja Laravel

{
"error": {
"code": "CONFLICTO_RESERVA",
"message": "El equipo ya está reservado en ese horario.",
"details": {}
}
}
code

Lo compara un programa

message

Lo lee una persona

details

Qué campo falló

Paso 4 — atrapar solo si puedes hacer algo

Section titled “Paso 4 — atrapar solo si puedes hacer algo”
Deshacer

Revertir lo que quedó a medias

Traducir

Convertir un error técnico en uno de dominio

Reintentar

Un servicio externo que falló una vez

catch (\Exception $e) { } // BLOCKER — STD-BE-E03 el error desaparece

Si no puedes hacer ninguna de las tres no lo atrapes deja que suba

DB::transaction(function () use ($datos) {
$prestamo = Prestamo::create($datos);
$prestamo->equipo->update(['disponible' => false]);
});
Sin transacción

Si falla la segunda línea queda un préstamo con el equipo marcado disponible

Con transacción

Si algo lanza una excepción PostgreSQL deshace las dos escrituras

Ejemplo ilustrativo · ReservaFIC no actualiza disponible al prestar

Traducir — error técnico a error de dominio

Section titled “Traducir — error técnico a error de dominio”
try {
$prestamo = $this->repositorio->crear($datos);
} catch (UniqueConstraintViolationException $e) {
throw new ConflictoDeReservaException((int) $datos['equipo_id']);
}

Si el motor rechaza un duplicado el usuario recibe el mismo 409 de siempre y no un 500

Ejemplo ilustrativo · solo aplica si existe una restricción única que lo provoque

El cliente

Código de estado · code · un mensaje genérico en el 500

El log

Clase de la excepción · archivo · línea · IDs del caso

[2026-11-10 10:03:12] local.ERROR: SQLSTATE[08006] connection to server failed
{"userId":7,"exception":"[object] (Illuminate\\Database\\QueryException ...)"}

Registro ilustrativo

Log::info('Reserva rechazada por conflicto de horario', [
'equipo_id' => $e->equipoId,
'user_id' => $request->user()?->id,
]);
Sí

Identificadores que permiten reconstruir el caso

Nunca

Contraseñas tokens o el cuerpo completo de la petición

NivelCuándo
infoEvento normal del negocio
warningAlgo raro que no impidió la operación
errorLa operación falló

Este tema se practica en el Laboratorio 2 — API REST del proyecto.

Continúa con 4. Pruebas automatizadas.