Exceções

Conceito Inovador

Durante a codificação, o programador se depara muitas vezes com a necessidade de fazer várias verificações antes de proceder ao real propósito do código para garantir que as operações seguintes não irão corromper o funcionamento da aplicação. Por exemplo, verificar que o arquivo que quer ler, de fato existe ou que a conexão à internet realmente está aberta. Quando se verifica que a condição não é cumprida, então o programa não tem como continuar pois as condições essenciais ao seu funcionamento não estão satisfeitas; diz-se que aconteceu uma Exceção.

O conceito de Exceção foi introduzida pela linguagem C++ para tentar libertar o programador de continuamente ter que resolver o que fazer quando uma condição essencial não se verifica e libertar o programador utilizador de uma biblioteca de saber verificar tudo o que tem que ser verificado ao usar essa biblioteca. Com o mecanismo de exceções o programador pode decidir o que fazer mais adiante no código. Este foi realmente um mecanismo inovador, que praticamente todas as linguagens adotaram desde então.

Note
Note que o conceito de exceção em software é muito semelhante ao conceito de interrupção em hardware

O mecanismo de lançamento e captura

Lançamento de Exceções

O conceito de exceção é baseado num mecanismo de lançamento e captura. Quando a condição de falha é identificada dentro de algum método, um objeto representando a exceção é criado e lançado. Note que é lançado e nao retornado. O método não termina; o seu fluxo é interrompido.

Para a maioria das linguagens o objeto que é criado e lançado tem que herdar de alguma classe especial que representa uma exceção e o compliador e o runtime, sabem tratar. Por exemplo, em Java essa classe é Throwable, em Javascript e TypeScript é Error.

Existem então duas formas de um método terminar:

  1. Retornando um resultado

  2. Lançando uma exceção

As linguagens normalmente oferecem palavras-chave para cada forma de terminar o método:

  1. return para retornar um resultado

  2. throw para lançar uma exceção

Tomemos este exemplo de um método:

int divide(int dividendo, int divisor){
    if (divisor == 0){
        throw new ArithmeticException("Divisor não pode ser zero");
    }
    return dividendo / divisor;
}

Tentar dividir por zero é uma condição de falha - que não queremos que aconteça -, então verificamos se b é zero, e se for, criamos uma exceção - no caso, ArithmeticException - e a lançamos usando throws. Note que se b não for zero, o programa continua normalmente e calcula o quociente e o retorna usando return.

Quando a exceção é lançada o fluxo normal do programa é interrompido e o controle volta ao método chamador. Se este método não capturar essa exceção ela será passada ao método que chamou o método chamador. Isso acontece assim, sucessivamente, até que a exceção seja capturada ou até que a exceção chegue na VM, caso em que será capturada automaticamente.

Lidar com exceções é decidir onde capturar quais exceções e o que fazer uma vez que elas são capturadas.

Captura de Exceções

Exceções são lançadas de dentro de métodos. Para capturar uma exceção as linguagens normalmente oferecem uma estrutrua de blocos conhecida como try-catch-finally que pode ser usada de três formas: try-cacth , try-finally e try-catch-finally

Try-Catch

Esta é a forma mais usada. Todas as chamadas a métodos que sabemos que podem lançar exceções colocamos dentro de um bloco com a palavra reservada try antes.

	try {
		// aqui executamos um, ou mais, métodos
        // que podem lançar execções.
        return divide(numeroDePessoas, numeroDeAssentos);
	}

O uso de try significa que sabemos que pode acontece uma exceção dentro do bloco, mas ainda não estamos capturando a exceção. O código para fazer isso colocamos dentro de chaves com a palavra reservada catch atrás. Para usar o block catch precisamos que o bloco marcado com try venha primeiro.

	try {
		// aqui executamos um, ou mais, métodos
        // que podem lançar execções.
        return divide(numeroDePessoas, numeroDeAssentos);
	} catch (ArithmeticException e){
        // aqui sabemos que isto significa que não ha assentos
        // para este caso, resolvemos simplesmente retornar zero
        return 0;
    }

Muitos tipos de exceções podem ser lançadas e nem sempre o mesmo código de tratamento serve para todos os tipos. Para informar qual o tipo de exceção que o código se destina a resolver colocamos uma declaração a seguir ao catch que indica o tipo de exceção que podemos tratar naquele código. O tipo é definido declarando uma classe específica - no caso ArithmeticException.

Podemos declarar mais do que um bloco catch . Isso é importante porque podemos ter vários tipos diferentes de exceção sendo lançados e necessitar de um tratamento específico para cada um.

O mecanismo de captura é sensivel ao tipo e segue a regra do É-UM (herança). Quando dizemos que estamos capturando ArithmeticException estamos dizendo que queremos capturar qualquer instancia dessa classe, diretamente, ou qualquer instancia de uma das suas classes filhas.

Se o mecanismo de captura é sensivel a herança e todas as exceções herdam de uma classe raiz, isto significa que podemos usar a classe raiz para capturar toda a exceção de qualquer tipo:

	try {
		// aqui executamos um, ou mais, métodos
        // que podem lançar execções.


	} catch (Throwable e){ // usando a classe raiz
        // aqui capturamos todas as exceções

    }

O problema é que ao capturar desta forma, não sabemos o que aconteceu e portanto não temos como lidar com o problema. Teriamos que lidar de uma forma geral. Então, por regra, não é sensato usar a classe raiz para definir a captura. Eis um exemplo mais realista do uso de try-catch

	try {
        // aqui executamos um método que tenta ler um arquivo
         leArquivo();
	}catch (FileNotFoundException e) {
	    // se o arquivo não existir esta exceção é lançada.
        // aqui colocamos a resolução se o arquivo não existir

	}catch (EOFException e) {
	    // quando esta exceção acontece significa que aconteceu
	    // um problema na leitura do arquivo.
        // aqui colocamos a resolução se o arquivo não pode ser lido
	} catch (IOException e) {
	    // uma outra exceção de I/O aconteceu.
	    // aqui colocamos a resolução geral
	}

Pode acontece que durante a tentativa de resolução do problema, cheguemos à conclusão que não podemos fazer mais nada e o problema é irresoluvel. Nesse caso, é possível usar throw, dentro do bloco catch para relançar a exceção que capturámos.

	try {
        // aqui executamos um método que tenta ler um arquivo
         leArquivo();
	}catch (FileNotFoundException e) {
	    // se o arquivo não existir esta exceção é lançada.
        // aqui colocamos a resolução se o arquivo não existir

        avisaUsuario("Arquivo não encontrado");
	}catch (EOFException e) {
	    // quando esta exceção acontece significa que aconteceu
	    // um problema na leitura do arquivo.
        // aqui colocamos a resolução se o arquivo não pode ser lido

        avisaUsuario("Arquivo corrompido");
	} catch (IOException e) {
	    // uma outra exceção de I/O aconteceu.
	    // mas não sabemos lidar com essa

        throw e; // relança a exceção para o método chamador.
	}

Tudo se passa como se ela nunca tivesse sido apanhada.

Note
Em algumas linguagens, como C#, utilizar throw e representa realmente relançar a exceção como se ela tivesse acontecido nesse ponto do código. Para relançar mantendo a informação original é necessário um comando diferente. Em C# é apenas 'throw' sem o argumento 'e'.

A ordem pela qual devemos colocar os blocos catch não é aleatória. Se usarmos classes de exceção de uma mesma hierarquia, a classe mais genérica tem que ser capturada depois das outras da sua descendência. No exemplo anterior, FileNotFoundException e EOFException derivam de IOException por isso ela é capturada depois das outras duas. Isto é assim porque o runtime irá comparar a classe da exceção que aconteceu com a classe declarada em catch e usar o primeiro bloco que for compatível. Se a classe mais genérica estiver antes, ela seria sempre a escolhida nunca dando chance de usar os outros blocos. Se você tentar fazer isso, o compilador irá reclamar, protegendo a lógica do mecanismo de tratamento.

Try-Finally

Por vezes, mesmo sabendo que os métodos que usamos lançam exceções, sabemos também que não podemos fazer nada para as resolver. Nesse caso, simplesmente não usamos o bloco try-catch. Mas, e se, mesmo acontecendo uma exceção existe um código que precisamos executar? É neste caso que usamos o bloco finally

Este tipo de problema é mais comum do que possa parecer. Por exemplo, se você está tentando escreve num arquivo e acontece um problema, o arquivo tem que ser fechado mesmo assim. Ou, se você tem uma conexão a banco de dados e acontece algum problema a conexão tem que ser fechada.

Para usar o bloco try-finally , começamos como envolver os métodos que podem lançar exceções como vimos antes, mas usamos um bloco finally em vez de um catch.

	try {
		// aqui executamos um método que pode lançar uma exceção que não
		// sabemos resolver
	} finally {
		// aqui executamos código que tem que ser executado, mesmo que um problema aconteça.
	}

Isto é muito útil, mas pense o que acontece se dentro do bloco try colocamos um return. Isso significa que algo tem que ser retornado para fora do método, mas significa também que o método acaba aí. Nenhum código pode ser executado depois de um return (o compilador vai-se queixar dizendo que o código seguinte é inalcançável). Isso é tudo verdade, exceto se esse código suplementar estiver dentro de um bloco finally . O código dentro do bloco finally não apenas é executado se uma exceção acontecer, mas também se o método terminar normalmente. É garantido que o código dentro do bloco finally sempre será executado, aconteça o que acontecer. Este é um outro uso importante deste bloco.

Try-Catch-Finally

Este bloco é apenas a conjunção dos anteriores. Apenas é necessário deixar claro que o bloco finally tem que ser declarado depois de todos os blocos catch A Listagem seguinte mostra o uso de todos os conceitos e palavras chave relacionados ao mecanismo de exceções.

	// faz uma consulta SQL ao banco retornando todos os produtos
	public List<Produto> queryAllProducts () throws SQLException {

	// Para podermos usar o objeto con dentro do try e do finally
	// precisamos declará-lo fora de ambos os blocos.
	 Connection con = null;
	 try {
          // obtém conexão. Não nos importa muito como.
          con = this.getConnection();

          // executa comando SQL
          ResultSet rs = con.createStament().executeQuery("SELECT * FROM PRODUTOS");

          // mapeia o ResultSet para uma lista de objetos
          List<Produto> resultado = mapResultSet(rs,Produto.class);

          // retorna o resultado.
          // O código no bloco finally ainda será executado.
          return resultado;

	 } catch (SQLException e) {
            // descobre se a falha se deve à tabela não existir no banco
            if (this.exceptionMeansTableMissing(e)){
                // realmente a tabela não exite no banco.
                // retorna uma lista vazia.
                return Collection.emptyList();
            } else {
                // não conseguimos resolver o problema.
                // relançamos a exceção
                throw e;
            }
	 } finally {
            // fecha a conexão
            con.close();
	 }
	}

Mecanismo de fechamento automático de recursos

O uso que vimos de try-catch-finally para fechar a conexão com o banco é bem comum. Acontece sempre que temos algum recurso que precia ser fechado/liberado quando o código termina. Arquivos, conexões com banco e transações são exemplos comuns.

Por ser comum, algunas linguagens oferecem um mecanismo espeficio para lidar com isso. Cada linguagem tem um nome diferente para esta fucnionalidade, se existir.

Note
Em Java é chamado try-with-resource em C# é chamado using. Isto, principalmente porque em Java é aproveitada a estrutrua try-catch-finally e em C# é usada outra keyword: using.

O código anterior, no exemplo, em Java ficaria assim:

	// faz uma consulta SQL ao banco retornando todos os produtos
	public List<Produto> queryAllProducts () throws SQLException {

	 // A conexão é obtida dentro de uma estrutura especial do try
	 try (Connection con = this.getConnection()){

          // executa comando SQL
          ResultSet rs = con.createStament().executeQuery("SELECT * FROM PRODUTOS");

          // mapeia o ResultSet para uma lista de objetos
          List<Produto> resultado = mapResultSet(rs,Produto.class);

          // retorna o resultado.
          // O código no bloco finally ainda será executado.
          return resultado;

	 } catch (SQLException e) {
            // descobre se a falha se deve à tabela não existir no banco
            if (this.exceptionMeansTableMissing(e)){
                // realmente a tabela não exite no banco.
                // retorna uma lista vazia.
                return Collection.emptyList();
            } else {
                // não conseguimos resolver o problema.
                // relançamos a exceção
                throw e;
            }
	 } // não é necessário o finally
	}

O código fica muito mais simples. Não precisamos mais declarar a variável de conexão fora do try e o finally acontece automaticamente. Isto é vantajoso porque se acontecer outro problema ao tentar fechar a conexão o Java trata para nós corretamente. Lidar com isso manualmente seria muito complexo.

Para este mecanismo funcionar o objeto criado em try () deve ser um recurso. Cada linguagem identifica esse tipo de objetos de forma diferente. Em Java, o objeto deve implementar a interface AutoCloseable.

Resumo

O mecanismo de exceções é baseado no lançamento e captura de objetos da classe Throwable . Este mecanismo é diferente do mecanismo de retorno de resultados invocado quando usamos return e por isso existe a palavra throw , que lança a exceção e dá início ao mecanismo de lançamento e captura de exceções.

Se queremos apanhar uma, ou mais exceções, que sabemos que podem ser originadas pelos métodos que invocámos basta envolver esses métodos dentro de um bloco de execução com palavra try . Isto indica ao compilador que os métodos dentro do bloco podem lançar exceções. Se isso realmente acontecer, então a exceção será passado para o bloco catch, por fim, o código dentro de finally será executado.

Podemos capturar e tratar exceções usando uma conjunção do blocos try-catch-finally . É garantido que o código no bloco finally sempre é executado, mesmo que exista uma exceção e mesmo que o bloco try contenha a instrução return . Esta funcionalidade especial do bloco finally é importante quando temos que fazer operações de limpeza, como fechar conexões, antes de sair do método e mesmo que não existam exceções.

Para casos de limpeza de recursos podemos ainda usar, se a linguagem permitir, o mecanismo de fechamento automático de recursos que é ainda mais eficaz que try-catch-finally.

Criando Exceções

Vimos como as linguagens orientadas a objetos implementam o conceito de Exceção o seu lançamento e captura. Vimos que para que o mecanismo funcione temos que criar um objeto de uma classe que herde de uma classe raiz especial que o runtime entende como a base de todas as instâncias de uma exceção.

Prefira a classe de exceção presente na plataforma

As linguagens já trazem no seu SDK um conjunto de exceções para os casos mais comuns. Sempre que possível, estas classes de exceção devem ser usadas. Isto ajuda a ter homogeneidade e a que outros programadores entendam o seu código e os problemas que acontecem por terem familiaridade com essas exceções em outros cenários e APIs.

Contudo, nem sempre será possivel cobrir todos os casos usando as exceções do SDK, ou até poderiamos,mas a exceção não seria especifica o suficiente.

Utilize a classe mais específica ao problema

O nome da classe de exceção sempre deve indicar o problema ocorrido. Muitas linguagens permitem que a exceção contenha um texto explicativo. Este texto não conta para a regra do mais específico. Por exemplo:

void doSomething(int count, User user){
   if (count < 0) {
       throw new IllegalArgumentException("Count é negativo");
   }
   if (user == null) {
       throw new IllegalArgumentException("User é nulo");
   }

   ....
}

Parece que estamos seguindo as regras. Escolhemos usar uma exceção do SDK - IllegalArgumentException (Java) - como manda a primeira regras, mas a razão do problema está especificada na mensagem, não na classe. Isto viola a segunda regras. Como alternativa podemos usar NullPointerException em vez da segunda já que estamos interessados em verificar a nulidade, mas não é comum, nem recomendado, capturar essa exceção, então é melhor construirmos exceções mais específicas.

class NegativeArgumentException extends IllegalArgumentException {}

class NullArgumentException extends IllegalArgumentException {}

E podemos agora escrever:

void doSomething(int count, User user){
   if (count < 0) {
       throw new NegativeArgumentException("count");
   }
   if (user == null) {
       throw new NullArgumentException("user");
   }

   ....
}

Sendo uma classe, poderiamos definir atributos - como o nome do campo com problemas - para passar mais informações ao código que for capturar e lidar com estas exceções (a implementação não é mostrada). Note que fizemos as exceções herdar de IllegalArgumentException que é uma exceção do SDK, mantendo o nosso código fiel à primeira regra. Pode ser que outro programador recebendo uma das novas exceções não saiba o que fazer, mas saberá o que fazer com uma IllegalArgumentException velha conhecida de outros programas.

Quando você tiver que lançar uma exceção, seja específico. Não lance exceções genéricas que significam tudo e nada simultaneamente. Não use texto para diferenciar circunstâncias. Use a exceção que melhor detalha o problema. Se nenhuma existir, crie a sua própria classe de exceção que seja específica o bastante.

O quê, onde e porquê

Para uma exceção ser específica, o seu nome deve revelar informação sobre o porquê. Os seus atributos devem revelar informação sobre o quê e o mecanismo da linguagem vai indicar o onde.

No nosso exemplo anterior, se capturamos uma NegativeArgumentException, podemos entender que aconteceu uma falha por que o argumento é negativo (se esperava que não fosse). O argumento que esta com problemas é o aquele contido no atributo de NegativeArgumentException, no caso count. Onde acontece o problema? Na linha 2 do método doSomething.

Se podemos responder às 3 perguntas a classe de exceção é completa.

Não deixe para os outros o que você pode lançar primeiro

No primeiro exemplo sobre a divisão vimos como testar o dividendo para lançar uma ArithmeticException se ele zero.

int divide(int dividendo, int divisor){
    if (divisor == 0){
        throw new ArithmeticException("Divisor não pode ser zero");
    }
    return dividendo / divisor;
}

Não se não fizemos essa verificação, o resultado será o mesmo, pois o próprio runtime lançará essa exceção se o divisor for zero.

int divide(int dividendo, int divisor){
    return dividendo / divisor;
}

Aqui é um exemplo muito simples, mas geralmente, um método chama muitos outros métodos e pode ser complicado entender qual deu problema. Então a regra é sempre verificar as condições de falha antes de continuar.

Detalhe exatamente o que o seu método faz e o que impediria de completar essa tarefa. Se alguma das condições de impedimento está presente lance imediatamente uma exceção explicando porque o método não pode fazer o seu trabalho. Não espere até que outro método auxiliar que você vai usar depois lance uma exceção incompreensível quando você pode lançar uma muito mais detalhada. Eis um exemplo:

// evite
public void leArquivo(File arquivo) throws IOException{

	// chama função auxiliar
	read(new FileInputStream(arquivo)) ; // lança IOException genérica

}
// prefira
public void leArquivo(File arquivo) throws IOException{

	if (arquivo == null){
		throw new NullArgumentException("arquivo");
	} else if (!arquivo.isFile()){
		throw new NotAFileException(arquivo); // é uma diretoria, não um arquivo.
	} else if (!arquivo.exists()){
		throw new MyFileNotFoundException(arquivo); // criará a mensagem a partir dos dados de arquivo
	}
	// chama função auxiliar
	read(new FileInputStream(arquivo)) ; // lança IOException genérica

}

A exceção MyFileNotFoundException é uma exceção própria que contém um atributo do tipo File para permitir processamento posterior. Neste caso não usamos a exeção FileNotFoundException do SDK, exatamente por ela não permitir indicar o arquivo como um atributo.

A importância desta regra é poder saber onde, e por quê, o problema aconteceu. Quanto mais cedo você lançar a exceção, mais perto fica o onde, mais informação pode ser capturada e mais claro fica o porquê.

Não lance a casa pela janela

É proveitoso e boa ideia documentar todos os tipos de exceção que cada método lança e em que circustâncias essas exceções são lançadas. Contudo, se não existir relação entre elas pode levar a cenários como este:

	public void algumMetodo() throws EstaException, AquelaException, UmaOutraException, OutraPossivelException, EAindaMaisUmaException {

Como várias exceções podem acontecer, todas são registradas. Isto é especifico, mas polui a interface do método além de ser um pesadelo para o método que chamar este criar um monte de catch. A opção é usar um conjunto menor de exceções que encapsulem aquelas.

As exceções realmente lançadas, são todas aquelas, mas na documentação agrupamos por familias. Eventualmente, em só uma familia.

	public void algumMetodo() throws TopoDaHerancaAException, TopoDaHerancaBException {

Capturando Exceções

Vemos como as linguagens nos permitem capturar exceções com try-catch-finally, mas singifica isso que sempre devemos capturar as exceções de todos os métodos que invocamos? A resposta é um taxativo não.

Não capture o que você não pode segurar

Como regra não devemos capturar exceções nunca. Devemos deixar que se propagem até à última camada. Na última camada, sim, devemos capturá-las nem que seja para loggar que aconteceram.

Quando um método que você está invocando lançar uma exceção, se você não sabe o que fazer com ela, simplesmente não faça nada. Deixe para quem entende. Se todos os métodos aplicarem esta regra, você não precisa ficar enchendo seu código com tratamentos imprestáveis e chatos. Mas atenção, também não seja irresponsável achando que sempre existirá outro método que cuide do seu problema. O método tem que conhecer o seu lugar no sistema. Se o método não tem a quem passar a batata quente então não lhe resta alternativa senão tratar o problema, ou pelo menos, reportar que o problema aconteceu.

A importância desta regra é minimizar o código que lida com exceções impossíveis de resolver, centralizando ações necessárias nesses casos, como apresentar uma mensagem ao usuário, reportar o evento para um registro (log), ou mesmo fechar a aplicação.

Note
Um padrão importante para lidar com exceções na última camada é o ExceptionHandler.
Note
Em Java, devido às exceções verificadas, cumprir esta regra não é tão simples quanto parece. A forma que pode parecer simples é declarar que o método chamador também lança a exceção do método chamado. No exemplo, deixamos leArquivo() lançar IOException porque read() lança essa exceção e não sabemos como tratá-la. Contudo, se este método fizesse parte de uma interface que não pode lançar IOException , então teríamos que encapsular esse tipo de exceção em outra que a interface define. Se não define nenhuma teríamos que encapsulá-la numa exceção nossa, não verificada. Esse encapsulamento pode ser um problema ao tentar aplicar regra de ser especifico.

Nunca log e lance

É um erro comum construir código como este:

	catch (SQLException e) {
  		LOG.error("Blablabla", e);
  		throw e;
	}

ou

	catch (IOException e) {
	  LOG.error("Blablabla", e);
	  throw new MinhaException("Blablabla outra vez", e);
	}

ou

	catch (Exception e) {
	  e.printStackTrace();
	  throw new MinhaException(""Blablabla outra vez"", e);
	}

Em todos eles estamos tentando registar que aconteceu uma exceção que de fato não estamos tratando naquele ponto.

Quando criamos uma exceção passando outra como parametros formamos uma cadeia de exceções conhecida como stack trace. Portanto, em qualquer momento futuro podemos sempre acessar qual foi a exceção original. Por isto, não precisamos registrar essa informação em cada passo intermédio.

Se ha dados que queremos registrar, adicionamos esses dados na exceção como atributos.

O único cenário onde é aceitável registrar e lançar é quando queremos capturar informação não relacionada à exceção, por exemplo, quem está logado ou quanto tempo demorou o processo…​ Enfim, quaisquer dados que não fariam sentido estarem na exceção lançada.

Nunca sequestre a Exceção

Se não podemos registrar e lançar, poderiamos pensar entao, em apenas registrar. Esta prática é conhecida como sequestro da exceção e não é boa ideia.

	try {
	  algumMetodo();
	} catch (Exception e) {
	  LOG.error("metodo falhou", e);
	}

O resto do programa não saberá que aconteceu um problema e não poderá lidar com as consequencias. Lembre-se de não capturar o que não pode segurar. Não use o try-catch para deixar a exceção propagar, ou encapsule a exceção em outra exceção mais específica.

Note
A única exceção a esta regra é na implementação do padrão ExceptionHandler onde o objetivo é realmente sequestrar a exceção e devolver para o utilizador uma resposta mais condizente.

Nunca elimine o rastro da excepção

Ao compor uma exceção na construção de uma nova exceção, como vimos, é formado o rastro de exceções - conhecido como stack trace. Esta informação é importante e vital que seja mantida ao longo dos lançamento, pois é nele que se inclui a informação de onde, no código a exceção original aconteceu.

Lembre-se que o que informa sobre a causa da exceção é o objeto da exceção e não a mensagem textual que ele contém. Então, não escreva assim:

	try {
	  alguMetodo();
	} catch (Exception e) {
		throw new MinhaException("Blablalbla: " + e.getMessage());
	}

Prefira:

	try {
	  alguMetodo();
	} catch (Exception e) {
		throw new MinhaException(e);
	}

Sempre passando a exceção original no contrutor na nova exceção.

Pode acontecer que a exceção recebida é do mesmo tipo que aquela que vai ser lançada. No exemplo, poderiamos encapsular uma MinhaException dentro de outra MinhaException. Por outro lado, a exceção MinhaException pode ter exceções filhas mais espeficias dependendo de qual exceção realmente estamos capturando. Para estes cenários considere usar um método estático de fábrica (padrão Static Factory Method), assim:

	try {
	  alguMetodo();
	} catch (Exception e) {
		throw MinhaException.wrap(e);
	}

e o método wrap seria:

public class MinhaException extends RuntimeException {

   public static MinhaException wrap(Exception cause){
      if (cause instanceof MinhaException me){
          return me;
      } else {
          return new MinhaException(e);
     }
   }

   private MinhaException(Exception cause){
     super(cause);
  }
}

Onde poderiamos por mais if s se existirem classes mais específicas.

Bibliografia

[1] Three Rules for Effective Exception Handling, Jim Cuching (http://today.java.net/pub/a/today/2003/12/04/exceptions.html)

[2] Best Practices for Exception Handling, Gunjan Doshi (http://www.onjava.com/pub/a/onjava/2003/11/19/exceptions.html)

Mais detalhes

Scroll to Top