¿Existen razones legítimas para devolver objetos de excepción en lugar de lanzarlos?

44

Esta pregunta está destinada a aplicarse a cualquier lenguaje de programación OO que admita el manejo de excepciones; Estoy usando C # solo con fines ilustrativos.

Por lo general, se pretende que las excepciones se generen cuando surge un problema que el código no puede manejar de inmediato y luego se captura en una cláusula catch en una ubicación diferente (generalmente un marco de pila exterior).

P: ¿Existen situaciones legítimas en las que las excepciones no se producen y se capturan, sino que simplemente se devuelven desde un método y luego se transmiten como objetos de error?

Esta pregunta se me ocurrió porque el método System.IObserver<T>.OnError de .NET 4 sugiere precisamente eso : excepciones que se pasan como objetos de error.

Veamos otro escenario, la validación. Digamos que estoy siguiendo la sabiduría convencional y que, por lo tanto, estoy distinguiendo entre un tipo de objeto de error IValidationError y un tipo de excepción separado ValidationException que se usa para informar errores inesperados:

partial interface IValidationError { }

abstract partial class ValidationException : System.Exception
{
    public abstract IValidationError[] ValidationErrors { get; }
}

(El System.Component.DataAnnotations namespace hace algo bastante similar.)

Estos tipos podrían emplearse de la siguiente manera:

partial interface IFoo { }  // an immutable type

partial interface IFooBuilder  // mutable counterpart to prepare instances of above type
{
    bool IsValid(out IValidationError[] validationErrors);  // true if no validation error occurs
    IFoo Build();  // throws ValidationException if !IsValid(…)
}

Ahora me pregunto si no podría simplificar lo anterior a esto:

partial class ValidationError : System.Exception { }  // = IValidationError + ValidationException

partial interface IFoo { }  // (unchanged)

partial interface IFooBuilder
{
    bool IsValid(out ValidationError[] validationErrors);
    IFoo Build();  // may throw ValidationError or sth. like AggregateException<ValidationError>
}

P: ¿Cuáles son las ventajas y desventajas de estos dos enfoques diferentes?

    
pregunta stakx 05.08.2013 - 14:56

8 respuestas

31
  

¿Hay situaciones legítimas en las que no se lanzan y capturan excepciones, sino que simplemente se devuelven desde un método y luego se pasan como objetos de error?

Si nunca se lanza, entonces no es una excepción. Es un objeto derived de una clase de excepción, aunque no sigue el comportamiento. Llamarlo una Excepción es puramente semántica en este punto, pero no veo nada malo en no lanzarla. Desde mi perspectiva, no lanzar una excepción es exactamente lo mismo que una función interna que lo atrapa y evita que se propague.

¿Es válida para una función devolver objetos de excepción?

Absolutamente. Aquí hay una breve lista de ejemplos donde esto puede ser apropiado:

  • Una fábrica de excepciones.
  • Un objeto de contexto que informa si hubo un error anterior como una excepción lista para usar.
  • Una función que mantiene una excepción capturada previamente.
  • Una API de terceros que crea una excepción de un tipo interno.

¿No está tirando mal?

Lanzar una excepción es algo así como: "Ir a la cárcel. ¡No pasar Go!" En el juego de mesa Monopoly. Le dice a un compilador que salte todo el código fuente hasta la captura sin ejecutar ninguno de esos códigos fuente. No tiene nada que ver con errores, informar o detener errores. Me gusta pensar en lanzar una excepción como una declaración de "súper retorno" para una función. Devuelve la ejecución del código a un lugar mucho más alto que el llamante original.

Lo importante aquí es entender que el verdadero valor de las excepciones está en el patrón try/catch , y no en la creación de instancias del objeto de excepción. El objeto de excepción es solo un mensaje de estado.

En su pregunta, parece estar confundiendo el uso de estas dos cosas: saltar a un controlador de la excepción y el estado de error que representa la excepción. Solo porque tomó un estado de error y lo envolvió en una excepción no significa que esté siguiendo el patrón try/catch o sus beneficios.

    
respondido por el cgTag 05.08.2013 - 17:06
51

Devolver excepciones en lugar de lanzarlas puede tener sentido semántico cuando tiene un método de ayuda para analizar la situación y devolver una excepción apropiada que luego es lanzada por la persona que llama (puede llamar a esto una "fábrica de excepciones"). Lanzar una excepción en esta función de analizador de errores significaría que algo salió mal durante el análisis en sí mismo, mientras que devolver una excepción significa que el tipo de error se analizó con éxito.

Un posible caso de uso podría ser una función que convierte códigos de respuesta HTTP en excepciones:

Exception analyzeHttpError(int errorCode) {
    if (errorCode < 400) {
         throw new NotAnErrorException();
    }
    switch (errorCode) {
        case 403:
             return new ForbiddenException();
        case 404:
             return new NotFoundException();
        case 500:
             return new InternalServerErrorException();
        …
        default:
             throw new UnknownHttpErrorCodeException(errorCode);
     }
}

Tenga en cuenta que lanzar una excepción significa que el método se usó mal o tuvo un error interno, mientras que devolver una excepción significa que el código de error se identificó con éxito.

    
respondido por el Philipp 05.08.2013 - 16:39
5

Conceptualmente, si el objeto de excepción es el resultado esperado de la operación, entonces sí. Los casos en los que puedo pensar, sin embargo, siempre involucran tirar el balón en algún momento:

  • Variaciones en el patrón "Probar" (donde un método encapsula un segundo método de lanzamiento de excepciones, pero detecta la excepción y, en cambio, devuelve un booleano que indica el éxito). Usted podría, en lugar de devolver un Booleano, devolver cualquier Excepción lanzada (nulo si tuvo éxito), lo que le permite al usuario final obtener más información al tiempo que conserva la indicación de éxito o falla sin límites.

  • Procesamiento de errores de flujo de trabajo. Similar en la práctica al método de "prueba de encapsulación de patrón", es posible que tenga un paso de flujo de trabajo resumido en un patrón de cadena de comando. Si un enlace en la cadena lanza una excepción, a menudo es más fácil capturar esa excepción en el objeto abstracto, luego devolverlo como resultado de dicha operación para que el motor de flujo de trabajo y el código real ejecutados como un paso de flujo de trabajo no requieran un intento extenso. -coger la lógica de su propia.

respondido por el KeithS 05.08.2013 - 16:59
5

Supongamos que ha ejecutado una tarea para ejecutarse en un grupo de subprocesos. Si esta tarea arroja una excepción, es un hilo diferente para que no la atrapes. El hilo donde fue ejecutado acaba de morir.

Ahora considere que algo (ya sea el código en esa tarea o la implementación del grupo de subprocesos) la excepción lo almacena junto con la tarea y considera que la tarea finalizó (sin éxito). Ahora puede preguntar si la tarea ha finalizado (y preguntar si se lanzó o puede volver a lanzarse en su hilo (o, mejor aún, una nueva excepción con el original como causa)).

Si lo haces manualmente, puedes notar que estás creando una nueva excepción, lanzándola y atrapándola, luego almacenándola, en diferentes subprocesos recuperando, atrapando y reaccionando en ella. Por lo tanto, puede tener sentido saltar y atrapar, simplemente almacenarlo y finalizar la tarea y luego preguntar si hay una excepción y reaccionar ante ella. Pero esto puede llevar a un código más complicado si en el mismo lugar puede haber realmente excepciones.

PS: esto se escribe con experiencia con Java, donde la información de seguimiento de la pila se crea cuando se crea una excepción, (a diferencia de C # donde se crea cuando se lanza). Por lo tanto, el enfoque de excepción no lanzada en Java será más lento que en C # (a menos que se haya creado previamente y se haya reutilizado), pero tendrá información de seguimiento de pila disponible.

En general, me mantendría alejado de la creación de una excepción y nunca la lanzaría (a menos que la optimización del rendimiento después del perfilado lo señale como un cuello de botella). Al menos en java, donde la creación de excepciones es costosa (seguimiento de pila). En C # es posible, pero IMO es sorprendente y, por lo tanto, debe evitarse.

    
respondido por el user470365 05.08.2013 - 17:47
1

Depende de su diseño, normalmente no devolvería las excepciones a la persona que llama, sino que las arrojaría y atraparía y las dejaría así. Normalmente el código se escribe para fallar temprano y rápido. Por ejemplo, considere el caso de abrir un archivo y procesarlo (Esto es C # PsuedoCode):

        private static void ProcessFileFailFast()
        {
            try
            {
                using (var file = new System.IO.StreamReader("c:\test.txt"))
                {
                    string line;
                    while ((line = file.ReadLine()) != null)
                    {
                        ProcessLine(line);
                    }
                }
            }
            catch (Exception ex) 
            {
                LogException(ex);
            }
        }

        private static void ProcessLine(string line)
        {
            //TODO:  Process Line
        }

        private static void LogException(Exception ex)
        {
            //TODO:  Log Exception
        }

En este caso, cometeríamos un error y dejaríamos de procesar el archivo tan pronto como se encontrara con un registro incorrecto.

Pero digamos que el requisito es que deseamos continuar procesando el archivo incluso si una o más líneas tienen un error. El código puede comenzar a verse así:

    private static void ProcessFileFailAndContinue()
    {
        try
        {
            using (var file = new System.IO.StreamReader("c:\test.txt"))
            {
                string line;
                while ((line = file.ReadLine()) != null)
                {
                    Exception ex = ProcessLineReturnException(line);
                    if (ex != null)
                    {
                        _Errors.Add(ex);
                    }
                }
            }

            //Do something with _Errors Here
        }
        //Catch errors specifically around opening the file
        catch (System.IO.FileNotFoundException fnfe) 
        { 
            LogException(fnfe);
        }    
    }

    private static Exception ProcessLineReturnException(string line)
    {
        try
        {
            //TODO:  Process Line
        }
        catch (Exception ex)
        {
            LogException(ex);
            return ex;
        }

        return null;
    }

Entonces, en este caso, devolvemos la excepción a la persona que llama, aunque es probable que no devuelva una excepción, sino que se produzca algún tipo de objeto de error, ya que la excepción se detectó y ya se resolvió. Esto no es perjudicial si se devuelve una excepción, pero otras personas que llaman pueden volver a lanzar la excepción, que puede no ser deseable ya que el objeto de excepción tiene ese comportamiento. Si deseaba, las personas que llaman tienen la capacidad de volver a lanzar y luego devolver una excepción; de lo contrario, elimine la información del objeto de excepción y construya un objeto de peso ligero más pequeño y devuélvalo. El fallo rápido suele ser un código más limpio.

Para su ejemplo de validación, probablemente no heredaría de una clase de excepción ni lanzaría excepciones porque los errores de validación podrían ser bastante comunes. Sería menos costoso devolver un objeto en lugar de lanzar una excepción si el 50% de mis usuarios no llenan un formulario correctamente en el primer intento.

    
respondido por el Jon Raynor 05.08.2013 - 22:01
1
  

P: ¿Existen situaciones legítimas en las que no se lanzan y capturan excepciones, sino que simplemente se devuelven desde un método y luego se pasan como objetos de error?

Sí. Por ejemplo, hace poco me encontré con una situación en la que encontré que las excepciones lanzadas en un servicio web de ASMX no incluían un elemento en el mensaje SOAP resultante, por lo que tuve que generarlo.

Código ilustrativo:

Public Sub SomeWebMethod()
    Try
        ...
    Catch ex As Exception
        Dim soapex As SoapException = Me.GenerateSoapFault(ex)
        Throw soapex
    End Try
End Sub

Private Function GenerateSoapFault(ex As Exception) As SoapException
    Dim document As XmlDocument = New XmlDocument()
    Dim faultDetails As XmlNode = document.CreateNode(XmlNodeType.Element, SoapException.DetailElementName.Name, SoapException.DetailElementName.Namespace)
    faultDetails.InnerText = ex.Message
    Dim exceptionType As String = ex.GetType().ToString()
    Dim soapex As SoapException = New SoapException("SoapException", SoapException.ClientFaultCode, Context.Request.Url.ToString, faultDetails)
    Return soapex
End Function
    
respondido por el Tom Tom 06.08.2013 - 00:09
1

En la mayoría de los idiomas (afaik), las excepciones vienen con algunos extras añadidos. Sobre todo, una instantánea del seguimiento de pila actual se almacena en el objeto. Esto es bueno para la depuración, pero también puede tener implicaciones de memoria.

Como ya se dijo @ThinkingMedia, realmente estás usando la excepción como un objeto de error.

En su ejemplo de código, parece que lo hace principalmente para reutilizar el código y evitar la composición. Personalmente, no creo que esta sea una buena razón para hacer esto.

Otra posible razón sería engañar al lenguaje para que le dé un objeto de error con un seguimiento de pila. Esto proporciona información contextual adicional a su código de manejo de errores.

Por otro lado, podemos suponer que mantener el seguimiento de la pila alrededor tiene un costo de memoria. P.ej. Si comienzas a coleccionar estos objetos en algún lugar, puedes ver un impacto desagradable en la memoria. Por supuesto, esto depende en gran medida de cómo se implementan los seguimientos de la pila de excepciones en el lenguaje / motor respectivo.

Entonces, ¿es una buena idea?

Hasta ahora no lo he visto usado o recomendado. Pero esto no significa mucho.

La pregunta es: ¿Es realmente útil el seguimiento de la pila para su manejo de errores? O, de manera más general, ¿qué información desea proporcionar al desarrollador o administrador?

El patrón típico de lanzamiento / prueba / captura hace que sea necesario tener un seguimiento de la pila, porque de lo contrario no tienes idea de dónde viene. Para un objeto devuelto, siempre proviene de la función llamada. El seguimiento de la pila puede contener toda la información que necesita el desarrollador, pero tal vez sea suficiente algo menos pesado y más específico.

    
respondido por el donquixote 20.12.2016 - 07:17
-4

La respuesta es sí. Consulte enlace y abra la flecha de la guía de estilo de C ++ de Google sobre las excepciones. Puede ver los argumentos que usaron para decidir en contra, pero diga que si tuvieran que hacerlo otra vez, probablemente hubieran decidido hacerlo.

Sin embargo, vale la pena señalar que el lenguaje Go tampoco utiliza las excepciones de forma idiomática, por razones similares.

    
respondido por el btilly 05.08.2013 - 18:40

Lea otras preguntas en las etiquetas