Está en la página 1de 9

Control de excepciones en Delphi

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

Control de excepciones Estructurado


Una excepción es una característica del sistema operativo que permite al programador
detener la ejecución de un proceso inmediatamente e interceptar esa parada en cualquier
punto de la pila de llamadas que desee. El control de excepciones estructurado es una
combinación de características del sistema operativo y de un buen diseño, que hace uso de
las excepciones para implementar aserciones en el momento de la codificación y,
sobretodo, asegura que se responda de manera correcta en caso de que estas aserciones no
se cumplan.
Estas aserciones comúnmente son precondiciones - se tienen que cumplir para que un
método tenga éxito. Por ejemplo, un método que borra un registro de una base de datos
puede tener como precondición que el usuario se haya conectado a la base de datos antes de
ejecutarlo. Como la interfaz de usuario del programa está estructurada de tal modo que el
método solamente se puede llamar después de conectarse, el programador puede asumir
que la precondición se cumplirá siempre. ¿Pero que pasaría si en el servidor de bases de
datos se produce un error y este deja de responder?
El programador podría solucionar este problema comprobando la conexión al servidor al
inicio de cada método. Pero pueden existir otras precondiciones y esto llevará a repetir
código, o el programador puede olvidarse de hacer todas las comprobaciones al escribir un
nuevo método.
El control de excepciones estructurado permite con soluciones elegantes para cada uno de
estos casos. Asegura que el método fallará si las precondiciones no se cumplen, es más,
fallará de tal modo que el programador podrá reconocer el error y responder en
consecuencia.
Buena parte del trabajo de consultoría que llevo a cabo incluye trabajar en proyectos que
están dando problemas (la mayoría de veces debido a un diseño pobre) y rescribiendo
código que no se programó bien la primera vez. Un de los errores de código más comunes
con el que me encuentro es el uso erróneo de las excepciones (algunas veces tristemente
erróneo). Para empezar, explicaré unas cuantas reglas sobre lo que no se ha de hacer en el
control de excepciones y por qué no se ha de hacer.

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.

No capturar todas las excepciones de manera genérica


A veces veo código como este:
try
códigoQuePuedeCausarAlgunError;
except
on E: Exception do
begin
MessageDlg(E.Message, mtWarning, [mbOK], 0);
end;
end;

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.

No buscar las excepciones


La creación de excepciones es un proceso muy costoso en términos de rendimiento, por lo
tanto no se deberían de estar creando por sistema.
Un contexto en el que acostumbra a usar excepciones como sistema de control es el
siguiente.
function StringIsInteger(aStr: string): Boolean;
var
Temp: integer;
begin
Result := True;
try
Temp := StrToInt(aStr);
except
Result := False;
end;
end;

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.

Cómo usar excepciones correctamente


Ahora que hemos visto algunas de las maneras en las que no se deben usar excepciones,
pasaremos a ver algunos consejos sobre como usar de manera correcta el sistema de manejo
de excepciones de Delphi. (N. del T.: y en realidad de cualquier entorno moderno).
Usar las excepciones para permitir que el código se ejecute si ser interrumpido por la
gestión de errores
Uno de los principales objetivos de la gestión de excepciones es permitir separar el código
de control de errores de aquel que forma la lógica de la aplicación. Con este sistema, es
posible escribir el código como si nunca se produjesen errores y encapsular ese código en
bloques try...except cuando se necesite tratar los errores que se produzcan. De esta manera
se consigue un código más eficiente, ya que no se tienen que estar haciendo continuos
controles sobre los parámetros y otros datos para asegurar la precondiciones antes de hacer
nada.
Una manera de conseguir este objetivo es centralizar el control de excepciones.
TApplication cuenta con un evento que permite esta técnica: el evento OnException. Es
posible usar este evento para tratar todas las excepciones, sean del tipo que sean, que no se
traten en otro punto de la aplicación. Se puede usar este evento para crear un registro de
errores, o controlar de un cierto modo algunas excepciones en concreto.
Los programadores de aplicaciones deberían capturar las excepciones.
Como se explica más adelante, las excepciones se deberían generar principalmente en el
código que forma parte de componentes y librerías. Cuando se escriben aplicaciones, no
hay mucha necesidad de crear y lanzar excepciones. Los programadores de aplicaciones
deberían centrar su atención en controlar las excepciones que se lanzar desde los
componentes y librerías.

Capturar solamente excepciones concretas.


Como comentábamos antes, un programador nunca se debería comer excepciones. En lugar
de esto, lo que se debe hacer es capturar únicamente excepciones concretas que puedan ser
razonablemente previsibles. Si se llevan a cabo un montón de operaciones matemáticas, es
razonable querer capturar excepciones del tipo EMathError. Si se están haciendo muchas
conversiones quizá se quiera capturar aquellas del tipo EConverError. Del mismo modo,
cuando se trabaja con bases de datos, es lógico capturar las excepciones que hereden de
EDataBaseError.
Pero incluso estos errores son demasiado generales. Por ejemplo, cuando trabajamos con
bases de datos, pueden producirse excepciones de clases descendientes de EDAtabaseError
cuando se toman acciones de base de datos determinadas. Es decir, si se está abriendo una
consulta, se deberían capturar tan solo aquellas excepciones que ocurren al abrir datasets y
no todas aquellas EDatabaseError.
Como decía antes, me encuentro con código que se come las excepciones porque el
programador (o su jefe, o alguien que no tiene las ideas claras) quiere que el usuario nunca
vea errores. La manera de solucionar esto es capturar la excepción en concreto que
queremos que el usuario no vea. Por ejemplo:
try
códigoQueLevantaUnEConvertError;
except
on E: EConvertError do
begin
// tratar aquí la excepción
end;
end;

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;

Y entonces se me ocurre declarar lo siguiente:


type
ENxDatabaseErrorMuyRaro = class(EDatabaseError)

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.

Lanzar excepciones propias.


Cuando se lanza una excepción, siempre se deberían ser propias. Por ejemplo, si tenemos
una librería en un fichero llamado NixUtils.pas, en el propio fichero se debería declarar:
NixUtilsException = class(Exception);

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.

Dejar ver los mensajes de las excepciones a los usuarios.


Si aparece la tentación de ocultar todos los errores a los tiernos ojos de los usuarios, es
necesario realizarse la siguiente pregunta: ¿qué es peor, que un usuario vea un mensaje de
error o permitir que la aplicación continúe como si no hubiese ocurrido nada, dejando tras
de sí cálculos erróneos o datos dañados con toda probabilidad?
Por desgracia, he visto mucho código que responde a esta pregunta ocultando el error. Es
por eso que al final existen un montón de controladores de excepciones vacíos que se
comen cualquier error que se produzca.
Las excepciones se producen porque algo es incorrecto. Ignorarlas puede causar resultados
imprevisibles. Comerse una excepción de memoria agotada (N. del T:en inglés, out of
memory) puede tener consecuencias desastrosas porque la aplicación (y los usuarios)
continuara como si nada hubiese ocurrido cuando, en realidad, lo sucedido es muy malo.
Los usuarios sienten pánico ante los cuadros de diálogo, como han indicado muchos
expertos en interfices de usuario, pero si el cuadro de dialogo otorga al usuario algo que
hacer entonces puede ser mucha más útil de lo que acostumbra.

Crear buenos mensajes de error.


Los mensajes de error standard que produce la VCL son muy dispersos y muy poco
informativos. Personalmente, mi favorito es el mensaje "List Index out of range" (N. del T:
"Índice de la lista fuera de Rango") que da exactamente cero información acerca de que
lista está fuera de rango. Como mínimo se podría comunicar el nombre de clase de la lista,
de tal manera que resultase un poco más fácil de encontrar, ¿no?
No es necesario hacer esto:
type
EUnaException = class(Exception);
procedure CausaUnaexcepción;
begin
raise EUnaException.Create('Mensage tonto');
end;

Cuando se puede hacer así


type
EUnaException = class(Exception);
procedure CausaUnaexcepción;
begin
raise EUnaException.Create('El procedimiento "CausaUnaexcepción" ' +
'no hace nada más que lanzar esta
excepción.' +
'¿Por qué diablos lo estás llamando?');
end;

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;

Como el controlador incluye la llamada a raise, la excepción no se oculta, pero el código da


más información acerca del error, etc. Podemos incluir información acerca de dónde puede
encontrar ayuda el usuario, qué puede hacer si el error persiste, etc.

Ofrecer dos versiones de cada rutina de librería


Existen programadores a los que no les gustan las rutinas que producen excepciones. Bien,
ok, ¿por no cumplir su capricho? A veces la VCL hace lo siguiente: ofrece dos funciones
que hacen lo mismo, una levanta una excepción si falla y la otra devuelve nil o algún
código de error, dependiendo del tipo de la rutina. Se me ocurre la pareja
FindClass/GetClass. FindClass busca y retorna una clase por su nombre y, si no la
encuentra, levanta una excepción. GetCalss, por otro lado, simplemente retornara nil si no
encuentra la clase.

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

También podría gustarte