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

45

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, las excepciones están destinadas a surgir cuando surge un problema que el código no puede manejar de inmediato, y luego quedar atrapado en una catchcláusula en una ubicación diferente (generalmente un marco de pila externo).

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

Esta pregunta se me ocurrió porque el System.IObserver<T>.OnErrormétodo de .NET 4 sugiere exactamente eso: las excepciones 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 IValidationErrory un tipo de excepción separado ValidationExceptionque se utiliza para informar errores inesperados:

partial interface IValidationError { }

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

(El System.Component.DataAnnotationsespacio de nombres 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, ¿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?

stakx
fuente
Si está buscando dos respuestas separadas, debe hacer dos preguntas separadas. Combinarlos solo hace que sea mucho más difícil de responder.
Bobson
2
@Bobson: Veo estas dos preguntas como complementarias: se supone que la primera pregunta define el contexto general del problema en términos generales, mientras que la segunda solicita información específica que permita a los lectores sacar su propia conclusión. Una respuesta no tiene que dar respuestas separadas y explícitas a ambas preguntas; las respuestas "intermedias" son bienvenidas.
stakx
55
No tengo claro algo: ¿Cuál es la razón para derivar ValidationError de Exception, si no vas a lanzarlo?
pdr
@pdr: ¿Quieres decir en el caso de tirar en su AggregateExceptionlugar? Buen punto.
stakx
3
Realmente no. Quiero decir que si vas a tirarlo, usa una excepción. Si lo va a devolver, use un objeto de error. Si esperaba excepciones que son realmente errores por naturaleza, agárrelas y colóquelas en su objeto de error. Si uno de los dos permite múltiples errores o no, es completamente irrelevante.
pdr

Respuestas:

31

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

Si nunca se lanza, entonces no es una excepción. Es un objeto derivedde 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 de malo en no lanzarlo. Desde mi punto de vista, no lanzar una excepción es exactamente lo mismo que una función interna que lo atrapa y evita que se propague.

¿Es válido que una función devuelva objetos de excepción?

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

  • Una fábrica de excepción.
  • 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 es malo tirarlo?

Lanzar una excepción es algo así como: "Ve a la cárcel. No pases ¡Ve!" en el juego de mesa Monopoly. Le dice a un compilador que salte sobre todo el código fuente hasta la captura sin ejecutar nada de ese código 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 "superretorno" para una función. Devuelve la ejecución del código a un lugar mucho más alto que el llamador original.

Lo importante aquí es comprender que el verdadero valor de las excepciones está en el try/catchpatrón, y no en la instanciación 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: un salto a un controlador de la excepción y el estado de error que representa la excepción. El hecho de que haya tomado un estado de error y lo haya envuelto en una excepción no significa que esté siguiendo el try/catchpatrón o sus beneficios.

Reactgular
fuente
44
"Si nunca se lanza, entonces no es una excepción. Es un objeto derivado de una Exceptionclase pero no sigue el comportamiento". - Y ese es exactamente el problema: ¿es legítimo usar un Exceptionobjeto derivado en una situación en la que no sea arrojado, ni por el productor del objeto, ni por ningún método de llamada; donde solo sirve como un "mensaje de estado" que se transmite, sin el flujo de control de "superretorno"? ¿O estoy violando un contrato try/ palabra catch(Exception)tan grave que nunca debería hacer esto, y en su lugar usar un tipo distinto Exception?
stakx
10
Los programadores de @stakx han hecho y continuarán haciendo cosas que son confusas para otros programadores, pero que técnicamente no son incorrectas. Por eso dije que era semántica. La respuesta legal es Noque no está rompiendo ningún contrato. La popular opinionsería que no deberías hacer eso. La programación está llena de ejemplos en los que su código fuente no hace nada malo, pero a todos no les gusta. El código fuente siempre debe escribirse para que quede claro cuál es la intención. ¿Realmente estás ayudando a otro programador usando una excepción de esa manera? Si es así, entonces hazlo. De lo contrario, no se sorprenda si no le gusta.
Reactgular
@cgTag El uso de bloques de código en línea para cursiva me duele el cerebro ...
Frayt
51

Devolver excepciones en lugar de lanzarlas puede tener sentido semántico cuando tiene un método auxiliar para analizar la situación y devolver una excepción apropiada que luego es lanzada por la persona que llama (podría llamar a esto una "fábrica de excepciones"). Lanzar una excepción en esta función del analizador de errores significaría que algo salió mal durante el análisis en sí, 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 convierta los 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ó incorrectamente o tuvo un error interno, mientras que devolver una excepción significa que el código de error se identificó con éxito.

Philipp
fuente
Esto también ayuda al compilador a comprender el flujo de código: si un método nunca regresa correctamente (porque arroja la excepción "deseada") y el compilador no sabe sobre eso, entonces a veces puede necesitar returndeclaraciones superfluas para que el compilador deje de quejarse.
Joachim Sauer
@JoachimSauer Sí. Más de una vez desearía que agregaran un tipo de retorno de "nunca" a C #: indicaría que la función debe salir lanzándola, poner un retorno es un error. A veces tienes un código cuyo único trabajo es encontrar una excepción.
Loren Pechtel
2
@LorenPechtel Una función que nunca regresa no es una función. Es un terminador. Llamar a una función como esa borra la pila de llamadas. Es similar a llamar System.Exit(). El código después de la llamada a la función no se ejecuta. Eso no suena como un buen patrón de diseño para una función.
Reactgular
55
@MathewFoscarini Considere una clase que tiene varios métodos que pueden generar errores de manera similar. Desea volcar alguna información de estado como parte de la excepción para indicar qué estaba masticando cuando se disparó. Desea una copia del código de mejora debido a DRY. Eso deja ya sea llamar a una función que hace el embellecimiento y usar el resultado en su lanzamiento o significa una función que construye el texto embellecido y luego lo arroja. Generalmente encuentro al último superior.
Loren Pechtel
1
@LorenPechtel ah, sí, yo también he hecho eso. Esta bien, lo entiendo ahora.
Reactgular
5

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

  • Variaciones en el patrón "Try" (donde un método encapsula un segundo método de lanzamiento de excepciones, pero captura la excepción y en su lugar devuelve un valor booleano que indica éxito). Podría, en lugar de devolver un booleano, devolver cualquier excepción lanzada (nula si tuvo éxito), permitiendo al usuario final más información mientras conserva la indicación de éxito o falla sin captura.

  • Procesamiento de errores de flujo de trabajo. Similar en la práctica a la encapsulación del método "probar patrón", puede tener un paso de flujo de trabajo abstraído en un patrón de cadena de comando. Si un enlace en la cadena arroja una excepción, a menudo es más claro detectar esa excepción en el objeto de abstracción, luego devolverlo como resultado de dicha operación para que el motor de flujo de trabajo y el código real ejecutado como un paso de flujo de trabajo no requiera un intento extenso -lógica de captura propia.

KeithS
fuente
No creo que haya visto personas notables que aboguen por tal variación en el patrón "Probar", pero el enfoque "recomendado" de devolver un booleano parece bastante anémico. Creo que sería mejor tener una OperationResultclase abstracta , con una boolpropiedad "Exitosa", un GetExceptionIfAnymétodo y un AssertSuccessfulmétodo (que arrojaría la excepción de GetExceptionIfAnysi no es nulo). Tener GetExceptionIfAnyun método en lugar de una propiedad permitiría el uso de objetos de error inmutables estáticos para casos en los que no había mucho que decir.
supercat
5

Digamos que ha realizado alguna tarea para que se ejecute en algún grupo de subprocesos. Si esta tarea arroja una excepción, es un subproceso diferente para que no la atrape. Hilo donde fue ejecutado simplemente muere.

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

Si lo hace manualmente, puede notar que está creando una nueva excepción, lanzándola y atrapándola, luego almacenándola, en diferentes hilos recuperando arrojándola, atrapando y reaccionando sobre ella. Por lo tanto, puede tener sentido omitir lanzar y atrapar y simplemente almacenarlo y finalizar la tarea y luego preguntar si hay una excepción y reaccionar al respecto. Pero esto puede conducir a un código más complicado si en el mismo lugar puede haber realmente excepciones.

PD: esto está escrito 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 al lanzar). Por lo tanto, el enfoque de excepción no lanzada en Java será más lento que en C # (a menos que se cree previamente y se reutilice), pero tendrá disponible información de seguimiento de pila.

En general, me mantendría alejado de crear excepciones y nunca lanzarlas (a menos que la optimización del rendimiento después de la elaboración del perfil 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.

user470365
fuente
Esto suele suceder también con los objetos de escucha de errores. Una cola ejecutará la tarea, detectará cualquier excepción y luego pasará esa excepción al responsable de escucha de errores. Por lo general, no tiene sentido matar el hilo de la tarea que no sabe mucho sobre el trabajo en este caso. Este tipo de idea también está integrada en las API de Java donde un hilo puede pasar excepciones de otro: docs.oracle.com/javase/1.5.0/docs/api/java/lang/…
Lance Nanek
1

Depende de su diseño, por lo general, no devolvería las excepciones a una persona que llama, sino que las lanzaría, atraparía y dejaría así. Por lo general, 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 detendríamos el procesamiento del archivo tan pronto como encontrara un registro incorrecto.

Pero supongamos que el requisito es que queremos 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 probablemente no devolvería una excepción, sino algún tipo de objeto de error, ya que la excepción se ha detectado y ya se ha solucionado. Esto no es perjudicial al devolver una excepción, pero otras personas que llaman pueden volver a lanzar la excepción, lo que puede no ser deseable ya que los objetos de excepción tienen ese comportamiento. Si desea que las personas que llaman tengan la capacidad de volver a lanzar y luego devolver una excepción, de lo contrario, saque la información del objeto de excepción y construya un objeto más pequeño y liviano y devuélvalo en su lugar. El error 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 o 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 completan un formulario correctamente en el primer intento.

Jon Raynor
fuente
1

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

Si. Por ejemplo, recientemente encontré una situación de situación en la que encontré que las excepciones lanzadas en un servicio web ASMX no incluyen 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
Tom Tom
fuente
1

En la mayoría de los idiomas (afaik), las excepciones vienen con algunos beneficios adicionales. Principalmente, una instantánea de la traza de la pila actual se almacena en el objeto. Esto es bueno para la depuración, pero también puede tener implicaciones de memoria.

Como ya dijo @ThinkingMedia, realmente está utilizando 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 hacerlo.

Otra posible razón sería engañar al lenguaje para darle 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 tiene un costo de memoria. Por ejemplo, si comienza a recolectar estos objetos en algún lugar, puede ver un impacto desagradable en la memoria. Por supuesto, esto depende en gran medida de cómo se implementen los rastreos de pila de excepciones en el idioma / motor respectivo.

Entonces, ¿es una buena idea?

Hasta ahora no he visto que se use o recomiende. Pero esto no significa mucho.

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

El patrón típico de lanzar / probar / atrapar 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.

Don Quijote
fuente
-4

La respuesta es sí. Consulte http://google-styleguide.googlecode.com/svn/trunk/cppguide.xml#Exceptions y abra la flecha de la guía de estilo C ++ de Google sobre excepciones. Puede ver allí los argumentos que solían decidir en contra, pero diga que si tuvieran que hacerlo de nuevo, probablemente lo habrían decidido.

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

btilly
fuente
1
Lo sentimos, pero no entiendo cómo estas pautas ayudan a responder la pregunta: simplemente recomiendan evitar por completo el manejo de excepciones. Esto no es lo que estoy preguntando; mi pregunta es si es legítimo usar un tipo de excepción para describir una condición de error en una situación en la que el objeto de excepción resultante nunca se lanzará. Parece que no hay nada de eso en estas pautas.
stakx