Try-Catch en Laravel: ¿Cuándo usarlo? Veamos algunos ejemplos prácticos

  • Publicado el 11 septiembre, 2024
  • Palabras: 2694

El operador try-catch de PHP es muy antiguo y todos conocemos su sintaxis. Pero lo que genera confusión es cuándo utilizarlo. ¿En qué casos? En este tutorial mostraré ejemplos prácticos para explicar la respuesta.

Try-Catch en Laravel: ¿Cuándo usarlo? Veamos algunos ejemplos prácticos

#Primer par de ejemplos: Eloquent VS JSON Decode

 

Permítanme explicar el try-catch con dos ejemplos opuestos.

 

#Ejemplo Eloquent: Echen un vistazo a este fragmento de código.

 

try {
    User::create($request->validated());
} catch (Exception $e) {
    return redirect()->route('users.create')
        ->with('error', 'Operation failed: ' . $e->getMessage());
}

 

Es un código válido en términos de sintaxis, pero ¿cuál es la probabilidad de que User::create() genere una excepción? Es bastante baja, ¿verdad?

 

Además, si los datos no son válidos, se debe detectar antes, en la fase de validación.

 

#Ejemplo JSON: Compárelo con este ejemplo del core del framework Laravel:

 

/**
 * Determine if a given value is valid JSON.
 *
 * @param  mixed  $value
 * @return bool
 */
public static function isJson($value)
{
    // ... some more code
 
    try {
        json_decode($value, true, 512, JSON_THROW_ON_ERROR);
    } catch (JsonException) {
        return false;
    }
 
    return true;
}

 

¿Sientes qué es fundamentalmente diferente en este ejemplo? Dos cosas:

 

  • No podemos "confiar" en el método json_decode() porque no lo creamos nosotros. Tampoco podemos confiar en el $value porque puede provenir de una entrada de usuario diferente. Por lo tanto, hay una gran posibilidad de que ocurra una excepción.
  • Además, queremos especificar qué hacer en caso de que ocurra esa excepción: en lugar de mostrar el error en la pantalla, decimos que no se reconoce JSON y devolvemos falso. Luego, quien llame a ese método isJson() decide qué hacer a continuación.


Si no ves la diferencia, intentaré replantearlo.

 

#Tres condiciones PRINCIPALES para utilizar Try-Catch

 

En base a estos ejemplos anteriores, así es como yo resumiría en qué casos deberías usar try-catch principalmente.

 

  1. Llamas a código "arriesgado": cuando tu código principal es una operación que no controlas completamente y es probable que genere una excepción. Ejemplos: API de terceros, paquetes externos, operaciones del sistema de archivos.
  2. Ya has validado los datos: cuando el mecanismo de validación de Laravel/PHP existente no detecta esa excepción por ti.
  3. Esperas la excepción: cuando realmente tienes un plan sobre qué hacer en caso de esa excepción, generalmente cuando se detecta una excepción específica.

 

En términos más mundanos, es como intentar conducir de A a B sin un mapa, sabiendo aproximadamente la ruta y el destino. Luego, INTENTAS conducir hasta allí, pero preparas un plan B (¿un seguro?) en caso de que te pierdas o tengas un accidente en algún lugar.

 

Lo sé, lo sé, tal vez no sea la mejor analogía, pero entiendes la idea :)

 

En otras palabras, try-catch se usa como una "póliza de seguro" en caso de un mayor riesgo de fallo. Es por eso que debes usarlo en casos específicos cuando realmente lo necesitas. No comprarías un seguro de accidentes si simplemente vas a pasear cerca de casa, ¿verdad?

 

#¿Qué hacer entonces en el "catch"? Veamos algunos ejemplos.

 

Si usas try-catch, necesitas tener un plan de lo que harás en caso de que ocurra una excepción. Por lo tanto, es hora de ver más ejemplos.

 

Los ejemplos de código provienen del propio framework de Laravel. Aprendamos de los mejores.

 

#Ejemplo: Devuelve falso/NULL u otro valor "de reserva"

 

El caso más típico que he encontrado es cuando los desarrolladores quieren asignar un valor predeterminado al valor de retorno del método, si algo sale mal. Vea este ejemplo:

 

src/Illuminate/Auth/Access/Gate.php:

 

class Gate implements GateContract
{
    // ...
 
    protected function methodAllowsGuests($class, $method)
    {
        try {
            $reflection = new ReflectionClass($class);
 
            $method = $reflection->getMethod($method);
        } catch (Exception) {
            return false;
        }
 
        if ($method) {
            $parameters = $method->getParameters();
 
            return isset($parameters[0]) && $this->parameterAllowsGuests($parameters[0]);
        }
 
        return false;
    }

 

Este método debe devolver un valor booleano, que puede ser verdadero solo si el parámetro es válido.

 

Devuelve falso en todos los demás casos:

 

  • Si el parámetro subyacenteAllowsGuests() devuelve falso
  • Si el $method está vacío
  • O... si ocurre CUALQUIER excepción al obtener ese método

 

Además, ese es un ejemplo interesante del llamado retorno anticipado. Por lo tanto, el mecanismo try-catch no necesariamente tiene que cubrir toda la función; puede usarse solo en una parte específica.

 

#Ejemplo: Mostrar mensaje de error "compatible con humanos"

 

Este también es un caso de uso típico: es posible que desee devolver su propio mensaje o página de error con estilo en caso de excepción.

 

Vea este ejemplo del comando “php artisan down”:

 

src/Illuminate/Foundation/Console/DownCommand.php:

 

class DownCommand extends Command
{
    public function handle()
    {
        try {
            // ... I deliberately skip some code for simplicity
 
            file_put_contents(
                storage_path('framework/maintenance.php'),
                file_get_contents(__DIR__.'/stubs/maintenance-mode.stub')
            );
 
            $this->components->info('Application is now in maintenance mode.');
        } catch (Exception $e) {
            $this->components->error(sprintf(
                'Failed to enter maintenance mode: %s.',
                $e->getMessage(),
            ));
 
            return 1;
        }
    }
}

 

En caso de excepción, aquí suceden dos cosas:

 

  • Se muestra un mensaje de error más fácil de entender
  • Se devuelve 1 como convención, lo que significa que el comando Artisan falló


Además, como puede ver, la operación en sí es una función "más arriesgada" file_put_contents(): no estamos seguros de si esa carpeta de almacenamiento existe y se puede escribir en ella. Por eso tiene sentido usar try-catch en primer lugar.

 

#Ejemplo: Reemplace la excepción con su excepción personalizada

 

Tal vez quieras capturar una excepción PHP/Laravel específica y reemplazarla con tu propia excepción personalizada que se encargue del manejo de errores a tu manera.

 

Este ejemplo proviene de Eloquent Relationships:

 

try {
    $relationship = $this->model->{$relationshipName}();
} catch (BadMethodCallException) {
    throw RelationNotFoundException::make($this->model, $relationshipName);
}

 

En este caso, estamos intentando reemplazar la BadMethodCallException de PHP con la RelationNotFoundException de Laravel que mostraría un mensaje más amigable para los humanos de "Call to undefined relationship [{$relation}] on model [{$class}]."

 

#Ejemplo: Realizar la acción antes de que se lance la excepción

 

A veces, es posible que queramos realizar una "limpieza" antes de que se ejecute la excepción y se finalice nuestro script.

 

Este ejemplo proviene del mecanismo de representación de Blade View:

 

src/Illuminate/View/View.php:

 

class View implements ArrayAccess, Htmlable, Stringable, ViewContract
{
    public function render(?callable $callback = null)
    {
        try {
            $contents = $this->renderContents();
 
            $response = isset($callback) ? $callback($this, $contents) : null;
 
            // Once we have the contents of the view, we will flush the sections if we are
            // done rendering all views so that there is nothing left hanging over when
            // another view gets rendered in the future by the application developer.
            $this->factory->flushStateIfDoneRendering();
 
            return ! is_null($response) ? $response : $contents;
        } catch (Throwable $e) {
            $this->factory->flushState();
 
            throw $e;
        }
    }
}

 

La operación de flushState() debe realizarse independientemente de si la representación fue exitosa.

 

Por cierto, ¿ves ese Throwable? No es una excepción, ¿verdad?

 

El problema es que hay una diferencia entre excepciones PHP y errores PHP.

 

Algunos ejemplos de errores pueden ser:

 

  • Recuento de argumentos incorrecto para la función
  • División por cero
  • TypeError (ya lo viste arriba)
  • ... y más, consulta la lista completa en esta página


Por lo tanto, se introdujo un Throwable como el interfaz base para cualquier objeto que se pueda lanzar a través de una declaración throw, incluidos TANTO errores como excepciones.

 

En el núcleo del framework Laravel, puedes encontrar muchos lugares donde intenta capturar Throwable para cubrir tanto excepciones como errores PHP.

 

#Ejemplo: Agregar datos adicionales a la excepción

 

Como un caso independiente de "hacer algo antes de que se ejecute la excepción", también podemos agregar algo a esa clase de excepción.

 

Este ejemplo proviene de Validation with Error Bag:

 

src/Illuminate/Validation/Validator.php:

 

class Validator implements ValidatorContract
{
    public function validateWithBag(string $errorBag)
    {
        try {
            return $this->validate();
        } catch (ValidationException $e) {
            $e->errorBag = $errorBag;
 
            throw $e;
        }
    }

 

Como puedes ver, solo estamos pasando el parámetro a la clase Exception.

 

#Ejemplo: Simplemente deje que ocurra la excepción

 

He visto este código muchas veces, especialmente por parte de desarrolladores principiantes: usar try-catch pero no hacer nada en el bloque catch.

 

Seré honesto: en el pasado, lo consideraba una "mala práctica". Pero, a lo largo de los años, me he encontrado con muchos casos en los que simplemente tiene sentido.

 

Entonces, esperas que aparezca una excepción pero quieres ignorarla, diciendo algo como "Ok, ok, sé que algo salió mal, pero aún así quiero que mi script continúe".

 

Este ejemplo proviene de la comparación de las rutas:

 

src/Illuminate/Routing/CompiledRouteCollection.php:

 

class CompiledRouteCollection extends AbstractRouteCollection
{
    /**
     * Find the first route matching a given request.
     */
    public function match(Request $request)
    {
        $matcher = new CompiledUrlMatcher(
            $this->compiled, (new RequestContext)->fromRequest(
                $trimmedRequest = $this->requestWithoutTrailingSlash($request)
            )
        );
 
        $route = null;
 
        try {
            if ($result = $matcher->matchRequest($trimmedRequest)) {
                $route = $this->getByName($result['_route']);
            }
        } catch (ResourceNotFoundException|MethodNotAllowedException) {
            try {
                return $this->routes->match($request);
            } catch (NotFoundHttpException) {
                //
            }
        }
 
        if ($route && $route->isFallback) {
            try {
                $dynamicRoute = $this->routes->match($request);
 
                if (! $dynamicRoute->isFallback) {
                    $route = $dynamicRoute;
                }
            } catch (NotFoundHttpException|MethodNotAllowedHttpException) {
                //
            }
        }
 
        return $this->handleMatchedRoute($request, $route);
    }

 

Dos cosas que destacar en este ejemplo:

 

  • En caso de que ocurran excepciones, el script seguirá ejecutando el método handleMatchedRoute() final en la parte inferior.
  • ¿Has notado que puedes hacer otro try-catch en el bloque catch del try-catch "principal"?

 

#Ejemplo: registrar la excepción únicamente

 

Este caso de uso es discutible. A menudo veo Log::xxxxx() en el bloque catch.

 

Este es un ejemplo típico:

 

try {
    // Code that may throw an Exception
} catch (Exception $e) {
    // Log the message locally
    Log::debug($e->getMessage());
 
    // Friendlier message to display to the user
    // OR redirect them to a failure page
}

 

Pero personalmente, no me parece práctico hacer try-catch manualmente solo para meterlo en un fichero log. Probablemente usaría herramientas externas específicas como Sentry o BugSnag: detectan excepciones automáticamente y las agrupan para procesarlas de una manera mucho más conveniente más adelante.

 

#Ejemplo: Try-catch... FINALLY?

 

No lo vemos muy a menudo, pero también está la tercera parte de try-catch.

 

Este ejemplo proviene del componente Artisan Console:

 

src/Illuminate/Console/View/Components/Task.php:

 

class Task extends Component
{
    public function render($description, $task = null, $verbosity = OutputInterface::VERBOSITY_NORMAL)
    {
        // ... skipped some code for simplicity
 
        try {
            $result = ($task ?: fn () => true)();
        } catch (Throwable $e) {
            throw $e;
        } finally {
            $runTime = $task
                ? (' '.$this->runTimeForHumans($startTime))
                : '';
 
            $runTimeWidth = mb_strlen($runTime);
            $width = min(terminal()->width(), 150);
            $dots = max($width - $descriptionWidth - $runTimeWidth - 10, 0);
 
            $this->output->write(str_repeat('<fg=gray>.</>', $dots), false, $verbosity);
            $this->output->write("<fg=gray>$runTime</>", false, $verbosity);
 
            $this->output->writeln(
                $result !== false ? ' <fg=green;options=bold>DONE</>' : ' <fg=red;options=bold>FAIL</>',
                $verbosity,
            );
        }
    }

 

¿Ves este enorme bloque de "finally"? En lenguaje humano, es esto: no importa si el bloque try tiene éxito o falla, el resultado se mostrará en la pantalla para el usuario.

Antonio Jenaro

Antonio Jenaro

Web Developer

Archivado en:

Fuente: Laravel Daily

Inicia la conversación

Hazte miembro de Antonio Jenaro para comenzar a comentar.

Regístrate ahora

¿Ya estás registrado? Inicia sesión