Documentos de Académico
Documentos de Profesional
Documentos de Cultura
Introducción
Volviendo al tiempo en que apareció Delphi, el control de excepciones supuso un cambio
fundamental tanto en la manera en que concebíamos como en la que escribíamos nuestro
código. Desafortunadamente, después de diez años de uso, aun existen errores de
concepción sobre cómo funcionan las excepciones y, especialmente, como deben ser
usadas.
En este artículo repasaré el modo en que se debe llevar a cabo el control de excepciones.
Empezaré con ejemplos de mal uso de las excepciones. A partir de estos ejemplos,
explicaré las mejores técnicas de gestión de excepciones. Usado incorrectamente, la gestión
fe excepciones puede ser la causa de más problemas de los que puede prevenir. Usado de
manera correcta, ayudará a escribir código limpio, bien diseñado y escalable. Daré por
supuesto que el lector está familiarizado con la sintaxis del control de excepciones y que
conoce lo fundamental acerca del sistema de excepciones
No comerse excepciones
Probablemente el error más común con el que me encuentro es comerse las excepciones.
Frecuentemente veo código como éste:
try
AlgunaRutinaQueAVecesCausaUnAccessViolationDificilDeEncontrar;
except
end;
¡Ep! Como se puede observar, este código se "comerá" cualquier excepción que sea lanzada
en la rutina a la que se llama. A menudo, el código en el bloque try ... except lanza una
excepción difícil de localizar, y en lugar de mostrar el lugar del error, el programador toma
el camino fácil y, sencillamente, se "come" la excepción. En ocasiones, el motivo para
comerse la excepción no es más que el deseo de que el usuario final no vea nunca un
mensaje de error. Si éste es el objetivo, en cualquier caso, se debería hacer de tal modo que
los errores no se ocultasen entre el resto del código, como mínimo.
Muy bien, estamos seguros de que el usuario final no verá ningún mensaje de error como
resultado de este código. Cada excepción que se pudiese producir en este código se
suprimirá (excepciones de base de datos, producidas por no haber memoria disponible,
errores de hardware, cualquiera). Esto quiere decir que el programa puede devolver datos
incorrectos mientras, en apariencia, ha tenido éxito. ¡Es mejor indicar claramente que se ha
producido un error que silenciarlo de manera que se pueda devolver un resultado
incorrecto!
En la única situación que me puedo imaginar en la que simplemente comerse una excepción
es aceptable es cuando se quiere evitar que la excepción se propague entre módulos. Si se
están programando módulos, o lo que es lo mismo, código que se ejecutará en una DLL,
ninguna excepción debería salirse de su módulo. En este casi, usar un controlador de
excepciones vacío puede conseguir este objetivo. Pero excepto en este caso, controladores
vacíos deberían ser considerados errores graves y código horroroso. Incluso en esta
situación, se debería hacer un registro de la excepción o guardarla de alguna manera.
Comerse una excepción significa tirar a la basura toda la información sobre el error (la cual
ayudaría a solucionar el problema). El cliente nunca se dará cuenta del error, y si lo hace, el
programador no será capaz de ver porqué ha sucedido y cómo solucionarlo. Moraleja:
simplemente no te comas excepciones.
Y pienso "Esto es como una dieta a base de coca-cola sin cafeína". Este código no hace
nada más que informar de la excepción de la que, de todas maneras, se informará. De
hecho, hace una cosa más y es detener la propagación de la excepción. La excepción se
controlará localmente, y nunca alcanzará más allá del ámbito local. Además, se capturarán
todas las excepciones, incluso aquellas que tal vez sería mejor no capturar.
En la única situación en la que se podría considerar usar este patrón (el cual es solo
ligeramente mejor que comerse la excepción) es cuando se sabe que la rutina que llamará a
la que se está programando no querrá nunca controlar ninguna excepción o tan solo alguna
específica. Por ejemplo, TClientDataset tiene el evento OnReconcileError al cual se le pasa
una excepción. Se está programando un proceso por lotes con un ClientDataset, permitir
que alguna excepción en este evento continúe y suba en la pila de llamadas provocará que
el proceso se detenga. En este caso sería aceptable capturar cualquier excepción que llegase
al evento.
Este código hará lo que se espera que haga (determinar si una cadena representa un entero
válido), pero también provocará que se lancen muchas excepciones, hecho que perjudicará
al rendimiento de la aplicación, especialmente si se espera que la función retorne "false" un
número elevado de veces.
Dejando a parte temas de rendimiento, éste es un uso incorrecto de las excepciones ya que
no se ha producido una violación de una precondición del método. El método esta
claramente diseñado para aceptar cadenas que no representen enteros, por lo tanto no se
puede lanzar una excepción, ni si quiera de manera interna, para controlar esta situación.
Una mejor implementación de este método podría ser usar la función TryStrToInt de la
unit SysUtils.
No usar la gestión de excepciones como sistema genérico de mensajes.
type
TSucesoNormalException = class(Exception);
…
begin
AlgoDecódigoQueHaceCosasNormalesYNoProduceErrores;
raise TSucesoNormalException.Create('Algo completamente normal ' +
'y que es el comportamiento
esperado');
end;
Puede existir la tentación de usar esta técnica para pasar algún tipo de información a la
rutina que llama a ésta, en especial si la nueva clase de excepciones contiene información
adicional que se le puede devolver. Es importante recordar es un sistema de control de flujo
además de una herramienta para transportar información. Lanzar una excepción cuando lo
que se desea es enviar un mensaje puede acarrear consecuencias inesperadas en el flujo de
ejecución del programa.
Utilizar las excepciones de este modo puede resultar muy molesto, incluso si la aplicación
siempre captura y controla la excepción en cuestión o si la clase hereda de EAbort y nunca
muestra una pantalla de error visible para el usuario. En primer lugar, este es un modo
costoso a nivel de procesador de pasar información. Segundo, el error aparecerá en tiempo
de diseño y hará que los programadores que usen el código se vuelvan locos (una famosa
librería de componentes externa a Borland usa esta técnica y siempre me hace perder
tiempo). Como norma general, si se lanza una excepción que en muchos casos hace que los
programadores la añadan a la lista de excepciones ignoradas por el IDE, se debería
replantear si realmente es necesario lanzarla.
Este código es mejor que comerse todas las excepciones, no importa lo que se haga con la
excepción, porque, en cualquier caso, solamente se capturará esta excepción y no todas las
que se podrían producir.
Es más, los errores de base de datos (y algunos otros como los COM) generalmente
incluyen un código de error, por lo tanto es posible capturar solamente aquellos con un
determinado código y no hacer nada con el resto. Esta técnica sigue el siguiente patrón:
try
códigoQueLevantaUnEConvertError;
except
on E: EIBError do
begin
if E.ErrorCode = UncódigoQueQuieroCapturar then
begin
// tratar aquí la excepción
end else
begin
raise;
// volvemos a lanzar la excepción, ya que no
//es la que queríamos controlar
end;
end;
end;
Otra razón para capturar las excepciones en la posición más baja de la jerarquía posible es
que en el futuro podrían crearse excepciones que desciendan de alguna de las clases más
genéricas. Por ejemplo, si tenemos este código:
try
UnDataset.Open
except
on E: EDatabaseError do
begin
// controlar la excepción
end;
end;
La nueva y rara excepción será capturada por el código original, y probablemente este no
sea el comportamiento deseado. Evidentemente es imposible prevenir este comportamiento
en todos los casos, ya que se pueden crear descendientes de cualquier excepción, pero si se
capturan las clases que estén en la parte más baja de la jerarquía, se consigue que suceda
menos.
Moraleja: Captura las excepciones tan abajo en la jerarquía de clases como sea posible y
captura tan solo aquellas que quieras controlar. Los programadores de componentes y
librerías lanzan excepciones.
Las excepciones no aparecen de manera misteriosa. La inmensa mayoría son creadas y
lanzadas dentro de la VCL. Y es totalmente aceptable que un programador lance sus
propias excepciones también.
Como norma general, se deberían lanzar excepciones propias dentro de componentes y
librerías. De este modo, los programadores de aplicaciones pueden capturar esas
excepciones específicas y tratarlas como se ha indicado antes.
Cuando se programan librerías y métodos de componentes, se debería hacer de manera que
solamente hubiesen dos casos posibles: o se ejecutan bien y acaban, o lanzan una
excepción. Los programadores de excepciones pueden asumir este comportamiento (que
una rutina que es llamada o finalizará de manera satisfactoria (con un resultado válido si se
trata de una función) o lanzará una excepción.
Y cualquier excepción que se lance en las rutinas del fichero deberá ser de esta clase o de
una descendiente.
De este modo se permite a los usuarios de la librería lo que he defendido antes: capturar tan
solo excepciones específicas. No hay que tener miedo de declarar excepciones propias y
derivarlas, incluso hasta el punto de tener clases concretas para cada rutina. Esto permitirá a
los usuarios de la librería el nivel de concreción que ellos deseen al capturar las
excepciones. Me encantaría que los autores de la VCL hicieran esto más a menudo. La
jerarquía de excepciones de la VCL es muy simple, y sería genial si se lanzarán
excepciones más específicas, especialmente en lo que se refiere al trabajo con bases de
datos, donde las excepciones se pueden producir (y de hecho se producen) en cualquier
punto.
En esto es mejor no imitar a los chicos de la VCL, es mejor escribir mensajes de errores
completos y descriptivos. Se puede incluir el nombre del procedimiento, el
TObject.ClassName, o lo que se desee en el mensaje.
De hecho, se puede mejorar el mensaje de error de la excepción y permitir que esta
continúe normalmente:
type
EExceptionConMensajeTonto = class(Exception);
procedure CrearexcepciónConMensajeTonto;
begin
raise EExceptionConMensajeTonto.Create('Boring message');
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
try
CrearexcepciónConMensajeTonto;
except
on E: EExceptionConMensajeTonto do
begin
E.Message := 'Este es un error causado de manera intencionada ' +
'en el evento Button1Click en la clase TForm1. El' +
'mensage original era: ' + E.Message;
raise;
end;
end;
end;
Conclusión.
Es muy fácil caer en la trampa de usar el control de excepciones de manera inadecuada. La
posibilidad de hacer que los errores y problemas "desaparezcan" es muy tentadora. En
cualquier caso, hacer un mal uso de las excepciones acarreará verdaderos problemas,
incluyendo la caída del sistema y la pérdida de información. El uso adecuado del control de
excepciones hará que el código sea más fácil de mantener. Un uso sabio de las excepciones
proporcionará un código robusto y limpio.
Agradecimientos
Estoy muy agradecido a Craig Stuntz, quien repasó el borrador de este artículo y realizó
algunas excelentes y valiosas contribuciones en él. Además, mucho de lo que sé acerca de
las excepciones y su control lo leí en "Delphi Component Design" de Danny Thorpe
Nick Hodges
Traducido por marto
Manel Martorana
21 de febrero de 2005