He visto en muchas ocasiones que hay como cierto miedo a tratar los errores, como si fuera algo complejo, difícil de controlar o directamente como si los errores "no fueran a ocurrir", que es peor, porque no se controlan y se confía ciegamente en la suerte.
En esta entrada voy a presentar una forma de trabajar con el Try/Catch que cumple dos objetivos distintos con una misma rutina:
- Poder relanzar el error para que lo trate un nivel superior
- Poder tratar el error localmente sin relanzarlo
¿Pero qué sentido tiene poder hacer esto? ¿Para qué puede servir llamarlo de una forma u otra?
Imaginemos dos situaciones distintas:
Situación 1: Tenemos una rutina en la que si ocurre un error, queremos mostrarlo para que el usuario esté notificado y nada más
Situación 2: Tenemos un método de negocio (procedimiento sin interfaz ni mensajes que se muestren al usuario que realiza alguna operación), donde de ocurrir un error, y al no poder mostrarlo, la única salida es reenviarlo hacia el nivel anterior para que sea tratado en el origen, por ejemplo, el botón de comando que originó todo, o un programa externo.
Normalmente esto requiere dos tratamientos distintos, pero hay una solución muy simple que permite utilizar una u otra de acuerdo a lo que nos convenga, que es la siguiente:
PROCEDURE Proc_X( toEx as Exception, tlRelanzarError, otros_params )
TRY
toEx = NULL
* Aquí va el proceso
CATCH TO toEx
IF tlRelanzarError
THROW
ENDIF
FINALLY
* Este IF/MESSAGE/ENDIF sólo debe ir si este es un método de Interfaz!
IF NOT ISNULL(toEx) AND NOT tlRelanzarError
MESSAGEBOX( "Error " + TRANSFORM(toEx.ErrorNo) + ", " + toEx.Message )
ENDIF
* Recolección de basura
ENDTRY
ENDPROC
En azul y amarillo están resaltados los dos parámetros que permiten esto. Por un lado, toEx guarda la información del error y por el otro tlRelanzarError indica si se desea, o no, relanzar el error al nivel superior. No hay más! Y así de simple como se ve, es sumamente versátil.
Veamos algunos ejemplos en la práctica.
Ejemplo 1: Tratamiento de errores local
En este caso se quiere controlar el error y mostrar un mensaje al usuario. Los métodos de este tipo suelen ser de la interfaz visual, y como se ve, es parecido al ejemplo original, pero más recortado y sin la parte de relanzar:
PROCEDURE Form1.cmdEjecutar.CLICK
TRY
LOCAL loEx as Esception
loEx = NULL
* Aquí va el proceso
CATCH TO loEx
FINALLY
IF NOT ISNULL(loEx)
MESSAGEBOX( "Error " + TRANSFORM(loEx.ErrorNo) + ", " + loEx.Message )
ENDIF
* Recolección de basura
ENDTRY
ENDPROC
Dentro de este mismo tipo de tratamiento tenemos otro caso, una actualización que solo actualiza un campo TIMESTAMP que se llama con un ON KEY LABEL F2 y que, por llamado con una tecla ON KEY, no permite otro tratamiento de errores que no sea local:
ON KEY LABEL F2 DO Actualiza WITH "", .F., 0
PROCEDURE Actualiza( toEx as Exception, tlRelanzarError, tnCount )
TRY
LOCAL lnCodError
lnCodError = 0
toEx = NULL
UPDATE USUARIO SET TIMESTAMP = DATETIME() WHERE USUA_PC = SYS(0)
tnCount = _TALLY
CATCH TO toEx
IF tlRelanzarError
THROW
ENDIF
lnCodError = toEx.ErrorNo
FINALLY
IF NOT ISNULL(toEx) AND NOT tlRelanzarError
MESSAGEBOX( "Error " + TRANSFORM(toEx.ErrorNo) + ", " + toEx.Message )
ENDIF
USE IN (SELECT("USUARIO"))
ENDTRY
RETURN lnCodError
ENDPROC
Como se ve, desde el DO Actualiza se está indicando .F. para tlRelanzarError, por lo que en el FINALLY, si ocurrió un error, se mostrará el mensaje con sus datos. Además en el tercer parámetro se pasa 0 porque no se podrá leer su valor.Ejemplo 2: Tratamiento de errores en nivel superior
En este caso vamos a usar los ejemplos anteriores, pero encadenados de tal forma que el segundo (la actualización) cambia su forma de trabajar para devolver el error y dejar que que sea el nivel superior quien muestre los datos del mismo:
PROCEDURE Form1.cmdEjecutar.CLICK
TRY
LOCAL loEx as Esception, lnCount
loEx = NULL
DO Actualiza WITH "", .T., lnCount
IF lnCount = 0
MESSAGEBOX( "No se actualizaron registros!" )
ENDIF
CATCH TO loEx
FINALLY
IF NOT ISNULL(loEx)
MESSAGEBOX( "Error " + TRANSFORM(loEx.ErrorNo) + ", " + loEx.Message )
ENDIF
* Recolección de basura
ENDTRY
ENDPROC
PROCEDURE Actualiza( toEx as Exception, tlRelanzarError, tnCount )
TRY
LOCAL lnCodError
lnCodError = 0
toEx = NULL
UPDATE USUARIO SET TIMESTAMP = DATETIME() WHERE USUA_PC = SYS(0)
tnCount = _TALLY
CATCH TO toEx
IF tlRelanzarError
THROW
ENDIF
lnCodError = toEx.ErrorNo
FINALLY
IF NOT ISNULL(toEx) AND NOT tlRelanzarError
MESSAGEBOX( "Error " + TRANSFORM(toEx.ErrorNo) + ", " + toEx.Message )
ENDIF
USE IN (SELECT("USUARIO"))
ENDTRY
RETURN lnCodError
ENDPROC
En este caso, si ocurriera un error en la rutina "Actualiza" se relanza el error (THROW) y se haría la recolección de basura (cerrar la tabla, limpiar vars, etc) tanto haya error o no.
El Throw salta hasta el CATCH del nivel anterior, que es el cmdEjecutar.Click() y recién ahí es donde se mostraría.
Así es como la misma rutina "Actualiza" se puede reutilizar controlando el error de dos formas distintas.
Control de errores compacto
Habiendo visto los casos de uso anteriores surgen varias ideas y optimizaciones para casos más concretos, por ejemplo, si sabemos de antemano que un método jamás mostrará un error porque es de negocio o para un proceso batch, entonces la estructura podría cambiar a esto:
PROCEDURE Actualiza( toEx as Exception, tlRelanzarError, tnCount )
TRY
LOCAL lnCodError
lnCodError = 0
toEx = NULL
UPDATE USUARIO SET TIMESTAMP = DATETIME() WHERE USUA_PC = SYS(0)
tnCount = _TALLY
CATCH TO toEx
IF tlRelanzarError
THROW
ENDIF
lnCodError = toEx.ErrorNo
FINALLY
USE IN (SELECT("USUARIO"))
ENDTRY
RETURN lnCodError
ENDPROC
Lo que sigue dejando 2 opciones:
- Llamar al método Actualiza relanzando el error, que lo redirigirá al Catch del nivel superior
- Llamarlo sin relanzar el error para evaluarlo localmente, como el siguiente caso
DO Actualiza WITH loEx, .F., lnCount
IF NOT ISNULL(loEx)
*-- Ocurrió un error, hacer algo
ELSE
*-- Ejecución OK
ENDIF
Achicando aún más
Finalmente hay una forma aún más compacta, asumiendo que solo se quiere relanzar el error y nunca se tratará como el caso anterior, sino directamente en el Catch del nivel superior que corresponda:
PROCEDURE Actualiza( tnCount )
TRY
LOCAL loEx as Exception
lnCodError = 0
loEx = NULL
UPDATE USUARIO SET TIMESTAMP = DATETIME() WHERE USUA_PC = SYS(0)
tnCount = _TALLY
CATCH TO loEx
THROW
FINALLY
USE IN (SELECT("USUARIO")) ENDTRY
RETURN
ENDPROC
Ayuda para Depurar en el control de errores
Esto es algo que no suele tenerse en cuenta, y que debería, ya que permite encontrar los errores mucho antes.
Cuando ocurre un error y se relanza la excepción hacia los niveles superiores, se pierde la información del entorno donde ocurrió (tablas abiertas, variables creadas, etc), ya que cada nivel fue cerrando y limpiando sus cosas, entonces se hace necesario una forma rápida que permita poder depurar "in-situ" cuando se está ejecutando en modo Desarrollo. Esta es una forma de hacerlo:
PROCEDURE Actualiza( tnCount )
TRY
LOCAL loEx as Exception
lnCodError = 0
loEx = NULL
UPDATE USUARIO SET TIMESTAMP = DATETIME() WHERE USUA_PC = SYS(0)
tnCount = _TALLY
CATCH TO loEx
IF _VFP.StartMode = 0 THEN
SET STEP ON
ENDIF
THROW
FINALLY
USE IN (SELECT("USUARIO"))
ENDTRY
RETURN
ENDPROC
Es así de simple y muy efectivo. Incluso se podría encapsular ese bloque en una función externa reutilizable, que también se encargue de generar un Log.
Una cosa importante sobre THROW:
No es lo mismo esto:
THROW
que esto:
THROW loEx
El primero relanzará el error tal cual, y se podrán seguir leyendo sus datos en loEx en cada Catch por el que pase, mientras que el segundo está relanzando específicamente un objeto Exception que al Catch de nivel superior le llegará en la propiedad UserValue, lo que lo hace más difícil de tratar si no se sabe esto previamente.
Información de error complementaria muy útil
Hay una propiedad del objeto Exception que suele pasarse por alto y que resulta ser muy útil para agregar información específica, que es UserValue. Obviamente que para aprovecharla se deben relanzar los errores con Throw solamente, o de otro modo se pisará su valor con una Excepción.Lo que le da un valor especial es que al ser una propiedad "para el usuario" podremos poner allí lo que querramos, tanto un valor como un objeto como un XML, lo que sea, y esto permite agregar más información específica del sitio donde ocurrió el error.
Por ejemplo, en el procedimiento AnalizarBloque_CDATA_inline() de FoxBin2Prg encontramos esto:
CATCH TO loEx
IF loEx.ErrorNo = 1470 && Incorrect property name.
loEx.UserValue = 'PropName=[' + TRANSFORM(tcPropName) + '], Value=[' ;
+ TRANSFORM(lcValue) + ']'
ENDIF
En este caso concreto me resulta muy importante adjuntar al objeto del error el nombre y el valor de la propiedad que puede causarlo, ya que si no, no saldría reflejado en ningún otro sitio o a lo sumo saldría solo el nombre de la propiedad.
Luego, en el nivel superior, junto la información de esta forma para mostrarla o loguearla a un archivo:
lcError = 'Error ' + TRANSFORM(toEx.ERRORNO) + ', ' + toEx.MESSAGE + CR_LF ;
+ toEx.Procedure + ', ' + TRANSFORM(toEx.LineNo) + CR_LF ;
+ toEx.LineContents + CR_LF + CR_LF ;
+ toEx.StackLevel + CR_LF + CR_LF ;
+ toEx.Detailt + CR_LF + CR_LF ;
+ EVL(toEx.UserValue,'')
En el método escribirArchivoBin() de la clase c_conversor_prg_a_dbf, guardo datos sobre el índice, el campo y la sentencia de creación de la tabla, ya que se intentará crear en ese método, pero si falla necesito saber qué ocurrió:
loEx.UserValue = 'lcIndex="' + TRANSFORM(lcIndex) + '"' + CR_LF ;
+ 'lcFieldDef="' + TRANSFORM(lcFieldDef) + '"' + CR_LF ;
+ 'lcCreateTable="' + TRANSFORM(lcCreateTable) + '"'
Para cada caso se debe contemplar adjuntar información específica para que ubicar el error, en caso de ocurrir, se haga lo más rápido posible.
Conclusión
Vimos algunas técnicas para controlar los errores, que es una de las cosas más importantes en cualquier aplicación, ya que de ello depende que aparezca un error descontrolado y que el usuario decida si "Aceptar, Cancelar o Ignorar" (lo peor que puede pasar, sobre todo si "Ignora"), o que el sistema tenga el flujo de errores definido desde el método más profundo para reenviarlo hasta llegar la interfaz o al programa externo.FoxBin2Prg, por ejemplo, usa el control compacto de errores, ya que no me interesa mostrar mensajes en cualquier momento, e incluso se puede configurar para no mostrar ningún mensaje y opcionalmente generar un log.
La recolección de basura también es muy importante, ya que no hacerlo de forma correcta puede llevar a un sistema inestable o que a la larga genere el temido error C0000005
Hasta la próxima!
Relacionados:
Técnicas de programación en VFP: Procesos desatendidos y control de progreso visual
MUY BUENA EXPLICACION DE TRY / CATCH!
ResponderEliminarMuy buenos ejemplos y explicación del Try / Catch y sobre todo como evitar el famoso error C000005!!!
ResponderEliminarSaludos!
Como Capturo el nombre del progama que genero el error
ResponderEliminarSi usas try/catch, en la propiedad PROCEDURE está el nombre del procedimiento que provocó el error, y LineNo te da el número de línea.
EliminarHay un punto ha tener en cuenta, despues de tu messagebox con el error dentro del try, al llegar al endtry sale la ventana de error de foxpro al menos que tengas ON ERROR * para ocultar todos los errores. Esto ultimo es un poco temerario si no tienes bien estructurada tu aplicacion con try/endtry. ¿alguna idea de como usar ambos sistemas?
EliminarSi te referís al MESSAGEBOX del "Ejemplo 2: Tratamiento de errores en nivel superior", si te fijás bien, este método es un teórico "PROCEDURE Form1.cmdEjecutar.CLICK", o sea que ya estamos en la interfaz y el manejador del CATCH atrapa el error que sea para que el FINALLY lo muestre. Por eso tampoco tiene un "RelanzarError"
Eliminar