¿Cuáles son las mejores prácticas para atrapar y volver a lanzar excepciones?

156

¿Deberían las excepciones capturadas volverse a lanzar directamente, o deberían envolverse alrededor de una nueva excepción?

Es decir, debería hacer esto:

try {
  $connect = new CONNECT($db, $user, $password, $driver, $host);
} catch (Exception $e) {
  throw $e;
}

o esto:

try {
  $connect = new CONNECT($db, $user, $password, $driver, $host);
} catch (Exception $e) {
  throw new Exception("Exception Message", 1, $e);
}

Si su respuesta es lanzar directamente , sugiera el uso del encadenamiento de excepciones , no puedo entender un escenario del mundo real en el que usamos el encadenamiento de excepciones.

Rahul Prasad
fuente

Respuestas:

287

No debe atrapar la excepción a menos que tenga la intención de hacer algo significativo .

"Algo significativo" podría ser uno de estos:

Manejando la excepción

La acción significativa más obvia es manejar la excepción, por ejemplo, mostrando un mensaje de error y abortando la operación:

try {
    $connect = new CONNECT($db, $user, $password, $driver, $host);
}
catch (Exception $e) {
    echo "Error while connecting to database!";
    die;
}

Registro o limpieza parcial

A veces no sabes cómo manejar adecuadamente una excepción dentro de un contexto específico; quizás le falta información sobre el "panorama general", pero desea registrar la falla lo más cerca posible del punto en el que ocurrió. En este caso, es posible que desee atrapar, iniciar sesión y volver a lanzar:

try {
    $connect = new CONNECT($db, $user, $password, $driver, $host);
}
catch (Exception $e) {
    logException($e); // does something
    throw $e;
}

Un escenario relacionado es donde se encuentra en el lugar correcto para realizar una limpieza de la operación fallida, pero no para decidir cómo se debe manejar la falla en el nivel superior. En versiones anteriores de PHP esto se implementaría como

$connect = new CONNECT($db, $user, $password, $driver, $host);
try {
    $connect->insertSomeRecord();
}
catch (Exception $e) {
    $connect->disconnect(); // we don't want to keep the connection open anymore
    throw $e; // but we also don't know how to respond to the failure
}

PHP 5.5 ha introducido la finallypalabra clave, por lo que para los escenarios de limpieza ahora hay otra forma de abordar esto. Si el código de limpieza necesita ejecutarse sin importar lo que sucedió (es decir, tanto por error como por éxito), ahora es posible hacerlo mientras se permite la propagación transparente de las excepciones lanzadas:

$connect = new CONNECT($db, $user, $password, $driver, $host);
try {
    $connect->insertSomeRecord();
}
finally {
    $connect->disconnect(); // no matter what
}

Abstracción de error (con encadenamiento de excepción)

Un tercer caso es donde desea agrupar lógicamente muchas posibles fallas bajo un paraguas más grande. Un ejemplo para la agrupación lógica:

class ComponentInitException extends Exception {
    // public constructors etc as in Exception
}

class Component {
    public function __construct() {
        try {
            $connect = new CONNECT($db, $user, $password, $driver, $host);
        }
        catch (Exception $e) {
            throw new ComponentInitException($e->getMessage(), $e->getCode(), $e);
        }
    }
}

En este caso, no desea que los usuarios Componentsepan que se implementa utilizando una conexión de base de datos (tal vez desee mantener abiertas sus opciones y utilizar el almacenamiento basado en archivos en el futuro). Por lo tanto, su especificación para Componentdiría que "en el caso de un error de inicialización, ComponentInitExceptionserá arrojado". Esto permite a los consumidores Componentdetectar excepciones del tipo esperado, al tiempo que permite que el código de depuración acceda a todos los detalles (dependientes de la implementación) .

Proporcionando un contexto más rico (con excepción de encadenamiento)

Finalmente, hay casos en los que es posible que desee proporcionar más contexto para la excepción. En este caso, tiene sentido incluir la excepción en otra que contenga más información sobre lo que estaba tratando de hacer cuando ocurrió el error. Por ejemplo:

class FileOperation {
    public static function copyFiles() {
        try {
            $copier = new FileCopier(); // the constructor may throw

            // this may throw if the files do no not exist
            $copier->ensureSourceFilesExist();

            // this may throw if the directory cannot be created
            $copier->createTargetDirectory();

            // this may throw if copying a file fails
            $copier->performCopy();
        }
        catch (Exception $e) {
            throw new Exception("Could not perform copy operation.", 0, $e);
        }
    }
}

Este caso es similar al anterior (y el ejemplo probablemente no sea el mejor), pero ilustra el punto de proporcionar más contexto: si se produce una excepción, nos dice que la copia del archivo falló. ¿ Pero por qué falló? Esta información se proporciona en las excepciones envueltas (de las cuales podría haber más de un nivel si el ejemplo fuera mucho más complicado).

El valor de hacerlo se ilustra si piensa en un escenario en el que, por ejemplo, la creación de un UserProfileobjeto hace que los archivos se copien porque el perfil de usuario se almacena en archivos y admite la semántica de transacciones: puede "deshacer" los cambios porque solo se realizan en un copia del perfil hasta que te comprometas.

En este caso, si lo hiciste

try {
    $profile = UserProfile::getInstance();
}

y como resultado se detectó un error de excepción "No se pudo crear el directorio de destino", tendría derecho a ser confundido. Ajustar esta excepción "principal" en capas de otras excepciones que proporcionan contexto hará que el error sea mucho más fácil de manejar ("Error al crear copia de perfil" -> "Error en operación de copia de archivo" -> "No se pudo crear el directorio de destino").

Jon
fuente
Estoy de acuerdo solo con las últimas 2 razones: 1 / manejo de la excepción: no debe hacerlo a este nivel, 2 / registro o limpieza: use finalmente y registre la excepción sobre su capa de datos
remi bourgarel
1
@remi: excepto que PHP no es compatible con la finallyconstrucción (al menos no todavía) ... Así que eso está fuera, lo que significa que debemos recurrir a cosas sucias como esta ...
ircmaxell 05 de
@remibourgarel: 1: Eso fue solo un ejemplo. Por supuesto, no debes hacerlo a este nivel, pero la respuesta es lo suficientemente larga como es. 2: Como dice @ircmaxell, no hay finallyen PHP.
Jon
3
Finalmente, PHP 5.5 ahora se implementa finalmente.
OCDev
12
Hay una razón por la que creo que te has perdido de tu lista aquí: es posible que no puedas saber si puedes manejar una excepción hasta que la hayas detectado y hayas tenido la oportunidad de inspeccionarla. Por ejemplo, un contenedor para una API de nivel inferior que usa códigos de error (y tiene millones de ellos) podría tener una única clase de excepción que arroje una instancia para cualquier error, con una error_codepropiedad que se puede verificar para obtener el error subyacente código. Si solo puede manejar significativamente algunos de esos errores, entonces probablemente quiera atrapar, inspeccionar, y si no puede manejar el error, vuelva a lanzar.
Mark Amery
37

Bueno, se trata de mantener la abstracción. Por lo tanto, sugeriría usar el encadenamiento de excepciones para lanzar directamente. En cuanto a por qué, permítanme explicar el concepto de abstracciones con fugas.

Digamos que estás construyendo un modelo. Se supone que el modelo abstrae toda la persistencia y validación de datos del resto de la aplicación. Entonces, ¿qué sucede cuando obtienes un error en la base de datos? Si vuelves a lanzar DatabaseQueryException, estás filtrando la abstracción. Para entender por qué, piensa en la abstracción por un segundo. No le importa cómo el modelo almacena los datos, solo que lo hace. Del mismo modo, no le importa exactamente qué salió mal en los sistemas subyacentes del modelo, solo sabe que algo salió mal y aproximadamente qué salió mal.

Entonces, al volver a lanzar DatabaseQueryException, está filtrando la abstracción y necesitando el código de llamada para comprender la semántica de lo que está sucediendo bajo el modelo. En cambio, cree un genérico ModelStorageExceptiony envuelva el atrapado DatabaseQueryExceptiondentro de eso. De esa manera, su código de llamada aún puede tratar de manejar el error semánticamente, pero no importa la tecnología subyacente del Modelo, ya que solo está exponiendo los errores de esa capa de abstracción. Aún mejor, ya que envolvió la excepción, si esta burbujea y necesita registrarse, puede rastrear hasta la excepción raíz lanzada (recorrer la cadena) para que todavía tenga toda la información de depuración que necesita.

No se limite a capturar y volver a lanzar la misma excepción a menos que necesite realizar un procesamiento posterior. Pero un bloque como no } catch (Exception $e) { throw $e; }tiene sentido. Pero puede volver a ajustar las excepciones para obtener una ganancia de abstracción significativa.

ircmaxell
fuente
2
Gran respuesta. Parece que algunas personas en Stack Overflow (basadas en respuestas, etc.) los están usando mal.
James
8

En mi humilde opinión, capturar una excepción para volver a lanzarlo es inútil . En este caso, simplemente no lo atrape y deje que los métodos llamados anteriormente lo manejen (también conocido como métodos que son 'superiores' en la pila de llamadas) .

Si lo vuelves a lanzar, encadenar la excepción atrapada en la nueva que lanzarás definitivamente es una buena práctica, ya que mantendrá la información que contiene la excepción atrapada. Sin embargo, volver a lanzarlo solo es útil si agrega alguna información o maneja algo a la excepción capturada, puede ser algún contexto, valores, registro, liberación de recursos, lo que sea.

Una manera de añadir un poco de información es extender la Exceptionclase, tener excepciones como NullParameterException, DatabaseException, etc. Más encima, esto permite que el revelador sólo para coger algunas excepciones que puede manejar. Por ejemplo, uno solo puede atrapar DatabaseExceptione intentar resolver lo que causó el Exception, como volver a conectarse al databse.

Clemente Herreman
fuente
2
No es inútil, hay momentos en los que necesita hacer algo con una excepción, por ejemplo, en la función que lo lanza y luego volver a lanzarlo para permitir que una captura superior haga otra cosa. En uno de los proyectos en los que estoy trabajando, a veces detectamos una excepción en un método de acción, mostramos un aviso amistoso al usuario y luego lo volvemos a lanzar para que un bloque de captura más adelante en el código pueda detectarlo nuevamente para registrar el error en un registro.
MitMaro
1
Entonces, como dije, agrega algo de información a la excepción (muestra un aviso, lo registra). No solo lo vuelves a lanzar como en el ejemplo del OP.
Clement Herreman
2
Bueno, puede volver a lanzarlo si necesita cerrar recursos, pero no tiene información adicional para agregar. Estoy de acuerdo en que no es la cosa más limpia del mundo, pero no es horrible
ircmaxell 05 de
2
@ircmaxell De acuerdo, editado para reflejar que es inútil solo si no haces nada, excepto volver a tirarlo
Clement Herreman
1
Lo importante es que pierdes el archivo y / o la información de la línea de dónde se lanzó originalmente la excepción al volver a lanzarla. Por lo tanto, generalmente es mejor tirar uno nuevo y pasar el viejo, como en el segundo ejemplo de la pregunta. De lo contrario, solo apuntará al bloque catch, dejándote adivinar cuál ha sido el problema real.
DanMan
2

Debe echar un vistazo a las mejores prácticas de excepción en PHP 5.3

El manejo de excepciones en PHP no es una característica nueva de ninguna manera. En el siguiente enlace, verá dos nuevas características en PHP 5.3 basadas en excepciones. El primero son las excepciones anidadas y el segundo es un nuevo conjunto de tipos de excepciones que ofrece la extensión SPL (que ahora es una extensión central del tiempo de ejecución de PHP). Ambas características nuevas han llegado al libro de las mejores prácticas y merecen ser examinadas en detalle.

http://ralphschindler.com/2010/09/15/exception-best-practices-in-php-5-3

HMagdy
fuente
1

Usualmente lo piensas de esta manera.

Una clase puede arrojar muchos tipos de excepciones que no coinciden. Entonces crea una clase de excepción para esa clase o tipo de clase y la arroja.

Entonces, el código que usa la clase solo tiene que atrapar un tipo de excepción.

Ólafur Waage
fuente
1
Oye, ¿puedes proporcionar más detalles o un enlace donde pueda leer más sobre este enfoque?
Rahul Prasad