Existem razões legítimas para retornar objetos de exceção em vez de jogá-los?


45

Esta pergunta se aplica a qualquer linguagem de programação OO que suporte o tratamento de exceções; Estou usando C # apenas para fins ilustrativos.

Geralmente, as exceções devem ser levantadas quando surgir um problema que o código não pode tratar imediatamente e, então, serem capturadas em uma catchcláusula em um local diferente (geralmente um quadro de pilha externo).

P: Existem situações legítimas em que as exceções não são lançadas e capturadas, mas simplesmente retornadas de um método e transmitidas como objetos de erro?

Essa pergunta surgiu para mim porque o System.IObserver<T>.OnErrormétodo do .NET 4 sugere exatamente isso: exceções sendo passadas como objetos de erro.

Vamos olhar para outro cenário, validação. Digamos que estou seguindo a sabedoria convencional e, portanto, estou distinguindo entre um tipo de objeto de erro IValidationErrore um tipo de exceção separado ValidationExceptionque é usado para relatar erros inesperados:

partial interface IValidationError { }

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

(O System.Component.DataAnnotationsespaço para nome faz algo bastante semelhante.)

Esses tipos podem ser empregados da seguinte maneira:

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(…)
}

Agora estou me perguntando, não poderia simplificar o acima para isso:

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: Quais são as vantagens e desvantagens dessas duas abordagens diferentes?


Se você estiver procurando por duas respostas separadas, faça duas perguntas separadas. Combiná-los apenas torna muito mais difícil responder.
Bobson

2
@Bobson: Eu vejo essas duas perguntas como complementares: A primeira pergunta deve definir o contexto geral da questão em termos gerais, enquanto a segunda pede informações específicas que devem permitir que os leitores tirem suas próprias conclusões. Uma resposta não precisa fornecer respostas explícitas e separadas para ambas as perguntas; as respostas "intermediárias" são igualmente bem-vindas.
stakx

5
Não estou claro sobre algo: Qual é o motivo para derivar ValidationError de Exception, se você não vai lançá-lo?
Pdr5

@pdr: Você quer dizer, no caso de jogar AggregateException? Bom ponto.
stakx

3
Na verdade não. Quero dizer que, se você for jogar, use uma exceção. Se você quiser devolvê-lo, use um objeto de erro. Se você esperava exceções que são realmente erros por natureza, identifique-as e coloque-as no seu objeto de erro. Se um deles permite ou não vários erros é totalmente irrelevante.
PDR

Respostas:


31

Existem situações legítimas em que as exceções não são lançadas e capturadas, mas simplesmente retornadas de um método e transmitidas como objetos de erro?

Se nunca for lançado, não será uma exceção. É um objeto derivedde uma classe Exception, embora não siga o comportamento. Chamar isso de exceção é puramente semântica neste momento, mas não vejo nada de errado em não lançá-lo. Na minha perspectiva, não lançar uma exceção é exatamente a mesma coisa que uma função interna que a captura e impede que ela se propague.

É válido que uma função retorne objetos de exceção?

Absolutamente. Aqui está uma pequena lista de exemplos onde isso pode ser apropriado:

  • Uma fábrica de exceção.
  • Um objeto de contexto que relata se houve um erro anterior como uma exceção pronta para usar.
  • Uma função que mantém uma exceção capturada anteriormente.
  • Uma API de terceiros que cria uma exceção de um tipo interno.

Não está jogando mal?

Lançar uma exceção é como: "Vá para a cadeia. Não passe em Go!" no jogo de tabuleiro Monopólio. Ele diz ao compilador para pular todo o código-fonte até a captura sem executar nenhum código-fonte. Não tem nada a ver com erros, relatórios ou interrupção de bugs. Eu gosto de pensar em lançar uma exceção como uma declaração de "super retorno" para uma função. Retorna a execução do código para um lugar muito superior ao chamador original.

O importante aqui é entender que o verdadeiro valor das exceções está no try/catchpadrão, e não na instanciação do objeto de exceção. O objeto de exceção é apenas uma mensagem de estado.

Na sua pergunta, você parece confundir o uso dessas duas coisas: pular para um manipulador da exceção e o estado de erro que a exceção representa. Só porque você tomou um estado de erro e o envolveu em uma exceção não significa que você está seguindo o try/catchpadrão ou seus benefícios.


4
"Se nunca for lançado, não será uma exceção. É um objeto derivado de uma Exceptionclasse, mas não segue o comportamento." - E esse é exatamente o problema: é legítimo usar um Exceptionobjeto derivado em uma situação em que ele não será lançado, nem pelo produtor do objeto nem por qualquer método de chamada; onde ele serve apenas como uma "mensagem de estado" sendo transmitida, sem o fluxo de controle de "super retorno"? Ou estou violando tanto um contrato não dito try/ não dito catch(Exception)que nunca devo fazer isso e, em vez disso, usar um Exceptiontipo não separado ?
stakx

10
Os programadores @stakx têm e continuarão a fazer coisas que são confusas para outros programadores, mas não tecnicamente incorretas. Por isso eu disse que era semântica. A resposta legal é que Novocê não está quebrando nenhum contrato. O popular opinionseria que você não deveria fazer isso. A programação está cheia de exemplos em que seu código-fonte não faz nada errado, mas todo mundo não gosta. O código-fonte deve sempre ser escrito para que fique claro qual é a intenção. Você está realmente ajudando outro programador usando uma exceção dessa maneira? Se sim, faça-o. Caso contrário, não se surpreenda se não gostar.
Reactgular

@cgTag O uso de blocos de código embutido para itálico faz o meu mal cérebro ..
Frayt

51

Retornar exceções em vez de lançá-las pode fazer sentido semântico quando você tem um método auxiliar para analisar a situação e retornar uma exceção apropriada que é lançada pelo chamador (você pode chamar isso de "fábrica de exceções"). Lançar uma exceção nessa função do analisador de erros significaria que algo deu errado durante a análise em si, enquanto retornar uma exceção significa que o tipo de erro foi analisado com êxito.

Um possível caso de uso pode ser uma função que transforma códigos de resposta HTTP em exceções:

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);
     }
}

Observe que lançar uma exceção significa que o método foi usado incorretamente ou teve um erro interno, enquanto retornar uma exceção significa que o código de erro foi identificado com êxito.


Isso também ajuda o compilador a entender o fluxo de código: se um método nunca retorna corretamente (porque lança a exceção "desejada") e o compilador não sabe disso, às vezes você pode precisar de returninstruções supérfluas para fazer com que o compilador pare de reclamar.
Joachim Sauer

@JoachimSauer Yeah. Mais de uma vez eu gostaria que eles adicionassem um tipo de retorno "nunca" ao C # - indicaria que a função deve sair lançando, colocar um retorno é um erro. Às vezes, você tem um código cujo único trabalho está gerando uma exceção.
Loren Pechtel

2
@LorenPechtel Uma função que nunca retorna não é uma função. É um terminador. Chamar uma função como essa limpa a pilha de chamadas. É semelhante a ligar System.Exit(). O código após a chamada da função não é executado. Isso não soa como um bom padrão de design para uma função.
Reactgular 5/08/13

5
@MathewFoscarini Considere uma classe que possui vários métodos que podem gerar erros de maneiras semelhantes. Você deseja despejar algumas informações de estado como parte da exceção para indicar o que estava mastigando quando explodiu. Você deseja uma cópia do código de compensação por causa de DRY. Isso deixa chamar uma função que faz a limpeza e usa o resultado em seu lançamento ou significa uma função que cria o texto embelezado e o lança. Eu geralmente acho o último superior.
Loren Pechtel

1
@ LorenPechtel ah, sim, eu também fiz isso. Ok, eu entendo agora.
Reactgular

5

Conceitualmente, se o objeto de exceção for o resultado esperado da operação, então sim. Os casos em que consigo pensar, no entanto, sempre envolvem arremesso de peso em algum momento:

  • Variações no padrão "Try" (onde um método encapsula um segundo método de lançamento de exceção, mas captura a exceção e, em vez disso, retorna um booleano indicando sucesso). Você pode, em vez de retornar um booleano, retornar qualquer exceção lançada (nula se for bem-sucedida), permitindo ao usuário final mais informações, mantendo a indicação de sucesso ou falha sem captura.

  • Processamento de erros no fluxo de trabalho. Semelhante na prática ao "tentar o padrão" do encapsulamento do método, você pode ter uma etapa do fluxo de trabalho abstraída em um padrão de cadeia de comando. Se um link na cadeia lança uma exceção, geralmente é mais fácil capturar essa exceção no objeto de abstração e devolvê-lo como resultado da operação mencionada, para que o mecanismo do fluxo de trabalho e o código real sejam executados como uma etapa do fluxo de trabalho, não exigindo uma tentativa extensiva. lógica de captura por conta própria.


Acho que não vi pessoas notáveis ​​advogar tal variação no padrão "Try", mas a abordagem "recomendada" de retornar um booleano parece bastante anêmica. Eu acho que seria melhor ter uma OperationResultclasse abstrata , com uma boolpropriedade "Successful", um GetExceptionIfAnymétodo e um AssertSuccessfulmétodo (que lançaria a exceção GetExceptionIfAnyse não for nulo). Tendo GetExceptionIfAnyser um método em vez de uma propriedade permitiria o uso de objetos de erro imutáveis estáticos para casos em que não havia muito útil a dizer.
21814

5

Digamos que você enquadrou alguma tarefa a ser executada em algum pool de threads. Se essa tarefa gerar exceção, será um encadeamento diferente para que você não o pegue. A linha onde foi executada simplesmente morre.

Agora considere que algo (o código nessa tarefa ou a implementação do conjunto de encadeamentos) captura essa exceção, armazene-o junto com a tarefa e considere a tarefa concluída (sem êxito). Agora você pode perguntar se a tarefa foi concluída (e perguntar se foi lançada ou se pode ser lançada novamente em seu encadeamento (ou melhor, nova exceção com o original como causa)).

Se você fizer isso manualmente, poderá perceber que está criando uma nova exceção, lançando-a e capturando-a e armazenando-a, em diferentes threads recuperando lançando, capturando e reagindo a ela. Portanto, pode fazer sentido pular o arremesso e a captura, armazená-lo e concluir a tarefa e depois perguntar se há exceção e reagir a ele. Mas isso pode levar a um código mais complicado, se no mesmo local houver realmente exceções.

PS: isso é escrito com experiência em Java, onde as informações de rastreamento de pilha são criadas quando a exceção é criada (ao contrário do C #, onde é criada ao lançar). Portanto, a abordagem de exceção não lançada em Java será mais lenta que em C # (a menos que seja pré-criada e reutilizada), mas terá informações de rastreamento de pilha disponíveis.

Em geral, eu evitaria criar exceções e nunca as lançaria (a menos que, como otimização de desempenho após a criação de perfil, aponte como gargalo). Pelo menos em java, onde a criação de exceções é cara (rastreamento de pilha). Em C #, é possível, mas a OMI é surpreendente e, portanto, deve ser evitada.


Isso também costuma acontecer com objetos do ouvinte de erros. Uma fila executará a tarefa, capturará qualquer exceção e passará essa exceção ao ouvinte de erro responsável. Normalmente, não faz sentido eliminar o encadeamento de tarefas que não conhece muito sobre o trabalho nesse caso. Esse tipo de idéia também é incorporado às APIs Java, nas quais um thread pode receber exceções passadas de outro: docs.oracle.com/javase/1.5.0/docs/api/java/lang/…
Lance Nanek

1

Depende do seu design, normalmente eu não retornaria exceções a um chamador, mas as jogaria e pegaria e deixaria assim. Normalmente, o código é escrito para falhar cedo e rapidamente. Por exemplo, considere o caso de abrir um arquivo e processá-lo (este é 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
        }

Nesse caso, teríamos um erro e pararíamos de processar o arquivo assim que ele encontrasse um registro incorreto.

Mas digamos que o requisito é que queremos continuar processando o arquivo mesmo que uma ou mais linhas apresentem um erro. O código pode começar a ter algo parecido com isto:

    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;
    }

Portanto, nesse caso, retornamos a exceção de volta ao responsável pela chamada, embora eu provavelmente não retornasse uma exceção de volta, mas algum tipo de objeto de erro, pois a exceção foi capturada e já foi tratada. Isso não faz mal ao retornar uma exceção de volta, mas outros chamadores podem repetir a exceção, o que pode não ser desejável, pois o objeto de exceção tem esse comportamento. Se você quiser que os chamadores tenham a capacidade de retornar novamente, retorne uma exceção; caso contrário, retire as informações do objeto de exceção e construa um objeto leve menor e, em vez disso, retorne-o. A falha rápida é geralmente um código mais limpo.

Para o seu exemplo de validação, eu provavelmente não herdaria de uma classe de exceção nem lançaria exceções porque os erros de validação podem ser bastante comuns. Seria menos caro retornar um objeto do que lançar uma exceção se 50% dos meus usuários falharem em preencher um formulário corretamente na primeira tentativa.


1

P: Existem situações legítimas em que as exceções não são lançadas e capturadas, mas simplesmente retornadas de um método e transmitidas como objetos de erro?

Sim. Por exemplo, recentemente encontrei uma situação em que descobri que exceções lançadas em um serviço da Web ASMX não incluíam um elemento na mensagem SOAP resultante, portanto tive que gerá-lo.

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

1

Na maioria dos idiomas (afaik), as exceções vêm com alguns itens adicionais. Principalmente, uma captura instantânea do rastreamento de pilha atual é armazenada no objeto. Isso é bom para depuração, mas também pode ter implicações de memória.

Como o @ThinkingMedia já disse, você realmente está usando a exceção como um objeto de erro.

No seu exemplo de código, parece que você faz isso principalmente para reutilizar o código e evitar a composição. Pessoalmente, não acho que essa seja uma boa razão para fazer isso.

Outro motivo possível seria enganar a linguagem para fornecer um objeto de erro com um rastreamento de pilha. Isso fornece informações contextuais adicionais ao seu código de tratamento de erros.

Por outro lado, podemos assumir que manter o rastreamento da pilha ao redor tem um custo de memória. Por exemplo, se você começar a coletar esses objetos em algum lugar, poderá ver um impacto desagradável na memória. Obviamente, isso depende muito de como os rastreamentos de pilha de exceção são implementados no respectivo idioma / mecanismo.

Então, é uma boa ideia?

Até agora, eu não o vi sendo usado ou recomendado. Mas isso não significa muito.

A pergunta é: o rastreamento de pilha é realmente útil para o tratamento de erros? Ou, de maneira mais geral, quais informações você deseja fornecer ao desenvolvedor ou administrador?

O padrão típico de throw / try / catch torna necessário um rastreamento de pilha, pois, caso contrário, você não tem idéia de onde ele é. Para um objeto retornado, ele sempre vem da função chamada. O rastreamento de pilha pode conter todas as informações que o desenvolvedor precisa, mas talvez algo menos pesado e mais específico seja suficiente.


-4

A resposta é sim. Consulte http://google-styleguide.googlecode.com/svn/trunk/cppguide.xml#Exceptions e abra a seta do guia de estilo C ++ do Google sobre exceções. Você pode ver lá os argumentos que eles usaram para decidir contra, mas diga que se eles tivessem que fazer isso novamente, provavelmente teriam decidido.

No entanto, vale ressaltar que o idioma Go também não usa exceções no idioma, por motivos semelhantes.


1
Desculpe, mas não entendo como essas diretrizes ajudam a responder à pergunta: Eles simplesmente recomendam que o tratamento de exceções seja completamente evitado. Não é disso que estou perguntando; minha pergunta é se é legítimo usar um tipo de exceção para descrever uma condição de erro em uma situação em que o objeto de exceção resultante nunca será lançado. Parece não haver nada sobre isso nessas diretrizes.
stakx
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.