Introdução
Desde a primeira versão do Delphi ele já trás o tratamento de exceções. Mesmo já fazendo muito tempo que essa funcionalidade acompanha o Delphi, isso não impediu que o tratamento de exceções fosse usado de maneira equivocada e errada até hoje.
Nesse post veremos como o tratamento de exceções deve ser feito de maneira correta. Para começar vou mostrar alguns exemplos de como NÃO usar esse recurso. Tendo esses exemplos em mente vamos discutir maneiras mais apropriadas de usar o tratamento de exceções. Uma coisa que a maioria dos programadores não percebe é que utilizar um tratamento de exceções de maneira errada pode causar mais problemas do que resolver. Utilizando esse recurso da linguagem de maneira correta o seu código fica mais fácil de ler e manter.
Tratamento de exceções estruturadas
A exceção é um recurso das linguagens de programação que permite ao programador criar um bloco de código que será executado se algum problema, não previsto acontecer. Com esse recurso o programador pode responder de maneira adequada ao erro quando ele acontece ou, até mesmo tratar esse erro.
A maioria dos programas segue uma sequencia de acontecimentos. Assim um determinado número de funções deve ser executado, corretamente, antes das funções seguintes. Um exemplo disso é uma função que excluí um registro de um banco de dados. Para que essa função seja executada corretamente é preciso que o usuário já esteja conectado ao banco de dados. Como as interfaces dos programas só disponibilizam a opção de excluir um registro depois que o usuário se conecta ao banco de dados, o programador pode deduzir que o usuário estará conectado ao banco de dados. Nessa hipótese o que acontece se o banco de dados apresentar uma falha e desconectar do usuário?
Uma solução para esse problema é verificar a conexão a cada chamada da função. Mas isso pode levar a duplicação de código, ou o programador pode simplesmente esquecer-se de fazer uma verificação.
Todos esses problemas podem ser resolvidos com o tratamento de exceções estruturadas. Uma exceção permite que a aplicação não pare de funcionar por apenas um erro. Além disso, é possível resolver o problema dentro de uma exceção.
Como não usar exceções
Uma boa parte do trabalho de um programador é em projetos existentes que estão com problemas (Isso por cauda de um projeto mal feito) e refazendo o trabalho que não foi bem feito na primeira vez. Um erro bem comum é o mau uso das exceções. Começaremos analisando os exemplos "Não faça", para ter uma noção de porque essas técnicas não são uma boa ideia.
Não esqueça as exceções
O uso mais comum e presente das exceções é o de esquecer o tratamento das exceções. Não é muito difícil encontrar códigos escritos como esse:
try
AlgumaRotinaQueAsVezesCausaUmaViolaçãoDeAcesso
except
end;
Nesse exemplo é possível ver que o programador está evitando qualquer exceção que possa aparecer. Não é raro uma rotina no bloco try ocasionar num erro que não é encontrado com facilidade e, em vez de seguir o caminho difícil (e correto) de encontrar o erro, o programador segue o caminho mais fácil e executa uma exceção. O que alguns desenvolvedores desejam com isso é que o usuário nunca veja uma mensagem de erro. Se esse é o seu objetivo você ainda pode conseguir isso sem esconder os erros do resto do código.
Com esse exemplo todas as exceções serão suprimidas - exceções de bando de dados, exceções de falta de memória, falhas de hardware, qualquer um desses. Desse modo o seu programa pode retornar com êxito quando, na verdade, deveria ter retornado com erro. Assim fica mais difícil encontrar um erro. É melhor tratar o erro da forma correta do que ter um erro silencioso que pode resultar no desmoronamento de todo o seu sistema.
Uma exceção pode ser aceitável quando você estiver trabalhando com módulos, desse modo é possível impedir que uma exceção afete todo o seu sistema. Um exemplo disso é com DLLs, nesse cenário não deixe que nenhuma exceção escape. Com uma exceção vazia no final de uma DLL esse problema seria resolvido. Mas, se esse não for o caso, não deixe blocos de exceções vazias. E mesmo nesses casos, você deve ser informado se algum erro ocorrer ou registrar esse erro. Ficar sem saber as informações dificulta a busca pela solução do problema. É possível que apenas "pulando" esse erro o seu cliente nunca descubra que existe um problema, mas também é possível que ele descubra, e sem as informações certas você não sabe o que aconteceu e muito menos como consertar.
Não intercepte exceções de forma genérica
Não é muito difícil encontrar códigos escritos dessa maneira:
try
AlgoQueTalvezCauseProblemas
except
on E: Exception do
begin
MessageDlg(E.Message, mtWarning, [mbOK], 0);
end;
end;
O que esse código faz é relatar um problema que de um jeito ou de outro seria relatado. Mas ele faz mais do isso, ele interrompe a exceção. A exceção será tratada no escopo local, dessa maneira ela não vai escapar do escopo atual. E ainda vai capturar todas as exceções, inclusive até aquelas que você pode não querer.
Esse código é um pouco melhor do que "pular" as exceções (só um pouco). Você pode considerar usar esse código quando chamar uma rotina que não vai lidar com nenhuma exceção ou quando ela for lidar com uma exceção especifica. Um exemplo disso é o evento OnReconcileError do TClientDataset que passa uma exceção. Quando um processo em lote do Clientdataset lança uma exceção isso pode interromper o loop. Desse modo é possível que você queira capturar todas as exceções de forma genérica.
Não procure exceções
Em termos de processamento as exceções são caras para criar e manipular, sendo assim você não deve criá-las sem um proposito. E não criá-las com o proposito, apenas, de verificar a ocorrência de um erro.
Você pode estar tentando fazer algo parecido com esse exemplo:
function StringIsInteger(str: string): Boolean;
var
Tmp: integer;
begin
Result := True;
try
Tmp := StrToInt(str);
except
Result := False;
end;
end;
Com esse código você consegue o que quer, mas é provável que gere muitas exceções. E isso pode trazer problemas para o desempenho da aplicação, especialmente quando a probabilidade do resultado for False for alto.
Deixando de lado o desempenho, esse exemplo faz mau uso do tratamento de exceções. Claramente a função foi projetada para aceitar parâmetros não inteiros. Por isso uma exceção não deve ser usada nesse caso. Uma maneira mais inteligente de resolver esse problema seria usar TryStrToInt
do SysUtils
.
Não use o tratamento de exceções como um sistema de sinalização genérico
type
TExceçãoDeComportamentoNormal = class(Exception);
...
begin
CoisasNormaisESemErros;
raise TExceçãoDeComportamentoNormal.Create('Algo perfeitamente normal' + 'e esperado aconteceu');
end;
Muitos desenvolvedores fazem uso do exemplo acima para sinalizar que ocorreu tudo bem na rotina. Você também pode se sentir tentado a fazer a mesma coisa, mas lembre-se que as exceções devem ser usadas para controlar o fluxo do programa em casos específicos e passar informações. Utilizar esse recurso para enviar uma mensagem de "que tudo ocorreu bem" pode ter consequências inesperadas.
Como usar exceções de maneira apropriada
Agora que você sabe algumas maneiras de como não usar exceções, aqui estão algumas dicas para usar corretamente as exceções do Delphi.
Use exceções para que seu código seja executado sem a Interrupção de tratamento de erros
Um dos objetivos das exceções é separar o código do seu programa do código de tratamento de algum erro eventual que possa acontecer na execução do seu programa. Desse modo é possível escrever o seu código como se nada fosse dar errado e, depois, separar o código com try
e except
para tratar algum problema desse código. Assim o seu código é executado com eficiência porque ele não está a todo o momento procurando por parâmetros errados para garantir que está tudo certo, para só então fazer alguma coisa com esses parâmetros.
Uma boa pratica é separar o código das exceções com TApplication
. Esse objeto possui um evento chamado OnException
que serve perfeitamente para esse tipo de situação. Com esse evento você pode lidar com todas as exceções que não forem tratadas pelo seu programa. OnException
permite você registrar as exceções ou aplicar um tratamento para cada tipo especifico de exceção.
Os desenvolvedores devem capturar as exceções
Como veremos abaixo, as bibliotecas e os componentes devem ser a principal fonte de exceções. Esses são os locais onde a maioria das exceções é criada. Escrevendo aplicativos a necessidade de criar exceções é baixa. Os desenvolvedores devem lidar com as exceções geradas pelos componentes e pelas bibliotecas.
Capture apenas exceções especificas
Como já falamos acima, nunca "pule" uma exceção. A coisa mais produtiva a se fazer é capturar apenas exceções especificas do seu código. Por exemplo, se você esta fazendo muitas conversões capture EConvertError
. Ou EMathError
, se você estiver utilizando muita matemática.
Como mencionado acima, existem programadores (ou gerente de projeto) que não querem que o usuário veja uma mensagem de erro. Isso não é um problema em se, o que causa o problema é "pular" exceções. Uma maneira de não deixar o usuário ver uma mensagem de erro e não "pular" essa exceção é capturar a exceção especifica que o usuário está vendo. Veja um exemplo:
try
CódigoQueVaiLançarEConvertError;
except
on E: EConvertError do
begin
// Trate o erro EConvertError aqui
end;
end;
Com esse código apenas uma exceção especifica será capturada e não todas. É uma coisa simples mais é melhor do que pular todas as exceções.
A maioria das exceções tem um código de erro, você pode capturar apenas erros com o código que lhe interessa e permitir que outros erros apareçam. Veja um exemplo de como isso pode ser feito:
try
LançaOErroEConvertError;
except
on E: EIBError do
begin
if E.ErrorCode = ErroQueQueroPegar then
begin
// Trate desse erro específico
end else
begin
raise; // Lança uma exceção se não for a única
end;
end;
end;
Um motivo para capturar as exceções no nível hierárquico mais baixo é que pode haver futuras exceções que descendem da classe Exception
. Veja um exemplo:
try
AlgumDataset.Open
except
on E: EDatabaseError do
begin
// Trate a exceção
end;
end;
e deposi ceclarar:
type
ENxEstranhoDatabaseError = class(EDatabaseError)
Essa exceção estranha será capturada pelo código, talvez isso não seja oque você queira. Certamente não é possível impedir que isso aconteça em cada acontecimento. Mas a frequência que acontece pode diminuir capturando exceções na parte mais inferior do código.
Conclusão: intercepte as exceções o mais longe possível na hierarquia de classes e intercepte apenas as exceções que você planeja manipular.
Componentes e bibliotecas lançam exceções
As exceções não aparecem do nada. A sua grande maioria é criada dentro do código. (Algumas poucas podem ser criadas fora do código Delphi) Mesmo assim é bom que você faça as suas próprias exceções.
Uma boa prática é criar exceções para cada erro dentro das suas bibliotecas e componentes. Assim outros desenvolvedores podem capturar essas exceções especificas.
Você deve escrever os seus códigos para que eles retornem apenas dois resultados: o resultado esperado ou uma exceção.
Deixe que os usuários vejam mensagens de exceção
Quando você esconde todas as exceções do usuário o que pode acontecer é o programa avançar sem ter concluído as tarefas e deixando dados corrompidos para trás. E o pior é que o usuário acha que tudo ocorreu bem.
Muitos especialistas em interface observaram que os usuários têm medo das caixas de diálogo. Um dos motivos é que elas não fornecem algo útil para o usuário fazer, o que a maioria faz é tipo isso: mostrar uma mensagem tipo "F@#$%... Você acaba de perder o trabalho de 2 horas" (Com certeza as mensagens não chegam nem perto disso, mas é o quê os usuários entendem. E estão errados!?) e um botão de OK. Uma coisa a se fazer é passar informações realmente uteis, para que o erro possa ser corrigido, isso de uma maneira amigável.
Boas mensagens para as exceções
Quando criar uma mensagem de erro sinta-se livre para detalhar o tipo de erro. Use mensagens curtas e diretas para informar o que acabou de ocorrer.
Não faça algo do tipo:
type
EAlgumaExcecao = class(Exception);
procedure CausaUmaExcecao;
begin
raise EAlgumaExcecao.Create('Mensagem qualquer');
end;
Quando você pode fazer algo parecido com isso:
type
EAlgumaExcecao = class(Exception);
procedure CausaUmaExcecao;
begin
raise EAlgumaExcecao.Create('Um erro ocorreu, e foi isso que aconteceu....');
end;
Ao escrever uma mensagem de erro descreva o que aconteceu de forma descritiva e completa. Nessa mensagem talvez você queira informar o nome do procedimento ou do objeto.
Conclusão
É muito mais fácil usar o tratamento de exceções de maneira incorreta. Com algumas linhas de código fazer um erro "desaparecer" é muito tentador. Mas, infelizmente, usar as exceções desse jeito pode causar mais problemas ao invés de fazer os existentes desaparecer. O uso correto do tratamento de exceções facilita a leitura do código e a sua manutenção. Use as exceções corretamente para criar um código limpo e organizado.