índice del curso de VB .NET

Curso de iniciación a la programación
con Visual Basic .NET

Entrega número ocho, (09/Dic/2002)
Publicada el 11/Dic/2002


Como prometí en la entrega anterior, (pero que no sirva de precedente, no sea que te acostumbres), en esta ocasión veremos cómo se interceptan los errores en Visual Basic .NET.

Cuando en el código de nuestra aplicación se produce un error sintáctico, es decir, porque hayamos escrito mal alguna instrucción de Visual Basic .NET, será el propio entorno de desarrollo el que nos avise de que hay algo que no es correcto; a este tipo de errores se suele llamar errores sintáctico o en tiempo de diseño. Pero si lo que ocurre es que hemos asignado un valor erróneo a una variable o hemos realizado una división por cero o estamos intentado acceder a un archivo que no existe, entonces, se producirá un error en tiempo de ejecución, es decir sólo sabremos que hay algo mal cuando el ejecutable esté funcionando.

Dos formas de interceptar errores

Control no estructurado de errores
Si conoces las versiones anteriores de Visual Basic, sabrás que la captura de errores se hacía con las instrucciones On Error. Si no has trabajado con ninguna versión anterior de Visual Basic, no te preocupes, ya que no tendrás porqué conocer esa forma de interceptar errores, ya que aquí no te lo voy a explicar... pero si aún así, quieres saber más sobre esas instrucciones para detectar errores, tendrás que acudir al curso Básico de VB (de mis páginas) o a otros artículos (también en este mismo sitio, en el que he tratado ese tema).
Seguramente te preguntarás porqué no quiero tratar esa forma de tratar errores... pues, simplemente porque no... (¡guille!) Bueno, seré algo más explícito... porque esa forma de tratar o interceptar errores no es la recomendable y es una forma, que si no se usa correctamente, nos puede ser más perjudicial que beneficiosa; y el uso de los tratamientos de errores es para que nos ayude, no para que nos cree más problemas... siempre te quedará el recurso de poder aprenderlo por tu cuenta, cosa que podrás hacer si es que realmente no te convence la nueva forma de tratar los errores, ya que a algunos puede que nos parezca menos útil que la forma "antigua", también llamada control no estructurado de errores.
No me enrollo más, otra cosa que debes saber, por si quieres indagar por tu cuenta, es que no se pueden mezclar en un mismo procedimiento las dos formas de tratar los errores.

Control estructurado de errores
El método recomendado de capturar errores en Visual Basic .NET, es usando la estructura Try Catch Finally.
La forma de usar esta estructura será algo así:

Try
    ' el código que puede producir error
Catch [tipo de error a capturar]
    ' código cuando se produzca un error
Finally
    ' código se produzca o no un error
End Try

En el bloque Try pondremos el código que puede que produzca un error.

Los bloques Catch y Finally no son los dos obligatorios, pero al menos hay que usar uno de ellos, es decir o usamos Catch o usamos Finally o, usamos los dos, pero como mínimo uno de ellos.
Una vez aclarado que, además de Try, debemos usar o Catch o Finally, veamos para que sirve cada uno de ellos:

Si usamos Catch, esa parte se ejecutará si se produce un error, es la parte que "capturará" el error.
Después de Catch podemos indicar el tipo de error que queremos capturar, incluso podemos usar más de un bloque Catch, si es que nuestra intención es detectar diferentes tipos de errores.

En el caso de que sólo usemos Finally, tendremos que tener en cuenta de que si se produce un error, el programa se detendrá de la misma forma que si no hubiésemos usado la detección de errores, por tanto, aunque usemos Finally (y no estemos obligados a usar Catch), es más que recomendable que siempre usemos una cláusula Catch, aunque en ese bloque no hagamos nada, pero aunque no "tratemos" correctamente el error, al menos no se detendrá porque se haya producido el error.
Aclaremos este punto, ya que puede parecer extraño.
Si decidimos "prevenir" que se produzca un error, pero simplemente queremos que el programa continúe su ejecución, podemos usar un bloque Catch que esté vacío, con lo cual el error simplemente se ignorará... si has usado o has leído sobre cómo funciona On Error Resume Next, pensarás que esto es algo parecido... si se produce un error, se ignora y se continúa el programa como si nada. Pero no te confundas que aunque lo parezca... no es igual. (guille, no te enrolles más y explícalo)
Si tenemos el siguiente código, se producirá una excepción (o error), ya que al dividir i por j, se producirá un error de división por cero.

Dim i, j As Integer
Try
    i = 10
    j = 0
    i = i \ j
Catch
	' nada que hacer si se produce un error
End Try
' se continúa después del bloque de detección de errores

Pero cuando se produzca ese error, no se ejecutará ningún código de "tratamiento" de errores, ya que dentro del bloque Catch no hay ningún código.
Seguro que pensarás (o al menos así deberías hacerlo), que eso es claro y evidente.
Si usáramos On Error Resume Next, el código podría ser algo como esto:

Dim i, j As Integer
On Error Resume Next
i = 10
j = 0
i = i \ j
' se continúa después del bloque de detección de errores

Bueno, si nos basamos en estos dos ejemplos, ambos hacen lo mismo: Si se produce un error se continúa justo por donde está el comentario ese que dice ' se continúa... pero... (¿de que te extrañas? ya deberías saber que "casi" siempre hay un pero), cuando se usan los bloques Try... Catch... el tratamiento de errores es diferente a cuando se usa el "antiguo" On Error Resume Next, para que nos entendamos, talvez este otro ejemplo aclare un poco las cosas:

Dim i, j As Integer
Try
    i = 10
    j = 0
    i = i \ j
    Console.WriteLine("el nuevo valor de i es: {0}", i)
Catch
    ' nada que hacer si se produce un error
End Try
' se continúa después del bloque de detección de errores
Console.WriteLine("después del bloque de detección de errores")

Aquí tenemos prácticamente el mismo código que antes, con la diferencia de que tenemos dos "instrucciones" nuevas, una se ejecuta después de la línea que "sabemos" que produce el error, (sí, la línea i = i \ j es la que produce el error, pero el que sepamos qué línea es la que produce el error no es lo habitual, y si en este caso lo sabemos con total certeza, es sólo es para que comprendamos mejor todo esto...), y la otra después del bloque Try... Catch.
Cuando se produce el error, el Visual Basic .NET, o mejor dicho, el runtime del .NET Framework, deja de ejecutar las líneas de código que hay en el bloque Try, (las que hay después de que se produzca la excepción), y continúa por el código del bloque Catch, que en este caso no hay nada, así que, busca un bloque Finally, y si lo hubiera, ejecutaría ese código, pero como no hay bloque Finally, continúa por lo que haya después de End Try.

¿Te ha quedado claro?
Te lo repito y te muestro lo que ocurriría en un bloque Try... Catch si se produce o no un error:
Si se produce una excepción o error en un bloque Try, el código que siga a la línea que ha producido el error, deja de ejecutarse, para pasar a ejecutar el código que haya en el bloque Catch, se seguiría (si lo hubiera) por el bloque Finally y por último con lo que haya después de End Try.
Si no se produce ningún error, se continuaría con todo el código que haya en el bloque Try y después se seguiría, (si lo hubiera), con el bloque Finally y por último con lo que haya después de End Try.

Ahora veamos el código equivalente si se usara On Error Resume Next:

Dim i, j As Integer
On Error Resume Next
i = 10
j = 0
i = i \ j
Console.WriteLine("el nuevo valor de i es: {0}", i)
' se continúa después del bloque de detección de errores
Console.WriteLine("después del bloque de detección de errores")

En este caso, cuando se produce un error, se continúa ejecutando el código, por tanto "se ejecutarán todas las líneas", entre otras cosas porque no hay ninguna distinción del código que se está "chequeando" del que no se comprueba.
Que te parece mejor solución, ya que así no te preocupas de nada cuando se produzca un error... ¡pues vale!
Pero... piénsatelo antes de usar un código como este... en el que al fin y al cabo no ocurriría nada extraño... pero si en lugar de ser un código simple, dependiéramos de que no se debe producir un error para continuar procesando el código, entonces lo tendríamos más complicado.

Veamos un ejemplo, que no será con código real, sino con "pseudo" código, el que abrimos un archivo (o fichero) de texto y en el que vamos a escribir el contenido de una o varias variables. Si no se produce error, todo irá bien, pero si se produce un error justamente cuando vamos a abrir el archivo, (por ejemplo, porque el disco esté lleno o no tengamos acceso de escritura o cualquier otra cosa), el archivo no estará abierto y por tanto lo que guardemos en él, realmente no se guardará.
El seudo código sería algo como esto:

Abrir el archivo
    Guardar el contenido de la variable
    repetir la línea anterior mientras haya algo que guardar
Cerrar el archivo
Avisar al usuario de que todo ha ido bien, que ya se puede ir a su casa a descansar porque todo se ha guardado.

Si usamos On Error Resume Next, se producirá un error al abrir el archivo, pero como le hemos dicho que queremos continuar, seguirá "palante"  e intentará guardar información en dicho archivo, pero como no puede guardar esa información, se volverá a producir un error, pero como continúa aunque haya errores, ni nos enteramos, pero se estarán produciendo errores de continuo y "no pasará nada" porque así lo hemos querido.
Y tu te preguntarás... ¿y que pasa? esa era la intención de usar lo de Resume Next. Pues sí, para que engañarnos, pero si lo que teníamos que guardar en el fichero ese eran 10000 líneas de texto, seguramente habremos desperdiciado un tiempo "precioso",a demás de que puede que el usuario ni se haya percatado de que ha estado esperando un buen rato y al final no se ha guardado la información.
Pero... como eres una persona que no se conforma con lo que yo digo y te has leído más cosas de "la otra forma" de detectar errores, seguramente me dirás que podrías haber usado On Error Goto nnn y haber mostrado un mensaje de aviso al usuario.
Pues ¡muy bien! Eso estaría bien, pero se sale de lo que aquí estamos, tratando, así que... ¡cállate! y ¡sigue leyendo!

Si en lugar de usar On Error Resume Next (e incluso On Error Goto nnn), hemos optado por usar Try... Catch, todo el seudo código ese, lo escribiríamos dentro del bloque Try y si se produce un error, avisaríamos al usuario de que se ha olvidado de poner el disquete, (por ejemplo), y le podríamos dar la oportunidad de volver a intentarlo...

Sí, ya se que eso mismo podríamos hacerlo con el On Error... pero hacerlo con Try resulta más elegante y no tendríamos la necesidad de usar un salto a una línea anterior usando una instrucción que... en fin... no sé si debería decirte que instrucción usarías... pero sí, es GoTo, la terrible instrucción que ha hecho del lenguaje Basic un lenguaje al que nadie ha tratado con "seriedad" y por culpa de la que este querido lenguaje de programación ha sido tan "blasfemado"... o casi, que tampoco es "pa" tanto, pero en fin...
De todas formas, hasta aquí hemos llegado con esto del Goto, que ni te voy a explicar para que sirve y también hemos llegado al final de todo lo que a tratamiento de errores no estructurados se refiere... por tanto, si quieres saber más de esas instrucciones... ya sabes, o te das una vuelta por algún "viejo" curso del VB o te lees lo que la documentación de Visual Basic .NET te diga, porque yo, no te voy a decir nada más de esas instrucciones...

Sigamos con el tratamiento de errores estructurados.

Como te he comentado antes, el bloque Catch sirve para detectar errores, incluso para detectar distintos tipos de errores, con idea de que el "runtime" de .NET Framework (el CLR), pueda ejecutar el que convenga según el error que se produzca.
Esto es así, porque es posible que un un bloque Try se produzcan errores de diferente tipo y si tenemos la "previsión" de que se puede producir algún que otro error, puede que queramos tener la certeza de que estamos detectando distintas posibilidades, por ejemplo, en el "seudo código" mostrado anteriormente, es posible que el error se produzca porque el disco está lleno, porque no tenemos acceso de escritura, porque no haya disco en la disquetera, etc. Y podría sernos interesante dar un aviso correcto al usuario de nuestra aplicación, según el tipo de error que se produzca.
Cuando queremos hacerlo de esta forma, lo más lógico es que usemos un Catch para cada uno de los errores que queremos interceptar, y lo haríamos de la siguiente forma:

Dim i, j As Integer
Dim s As String
'
Try
    Console.Write("Escribe un número (y pulsa Intro) ")
    s = Console.ReadLine
    i = CInt(s)
    Console.Write("Escribe otro número ")
    s = Console.ReadLine
    j = CInt(s)
    '
    Console.WriteLine("El resultado de dividir {0} por {1} es {2}", i, j, i \ j)
    '
Catch ex As DivideByZeroException
    Console.WriteLine("ERROR: división por cero")
Catch ex As OverflowException
    Console.WriteLine("ERROR: de desbordamiento (número demasiado grande)")
Catch ex As Exception
    Console.WriteLine("Se ha producido el error: {0}", ex.Message)
End Try
'
Console.ReadLine()

Aquí estamos detectando tres tipos de errores:
El primero si se produce una división por cero.
El segundo si se produce un desbordamiento, el número introducido es más grande de lo esperado.
Y por último, un tratamiento "genérico" de errores, el cual interceptará cualquier error que no sea uno de los dos anteriores.

Si usamos esta forma de detectar varios errores, te comentaré que debes tener cuidado de poner el tipo genérico al final, (o el que no tenga ningún tipo de "error a capturar" después de Catch), ya que el CLR siempre evalúa los tipos de errores a detectar empezando por el primer Catch y si no se amolda al error producido, comprueba el siguiente, así hasta que llegue a uno que sea adecuado al error producido, y si da la casualidad de que el primer Catch es de tipo genérico, el resto no se comprobará, ya que ese tipo es adecuado al error que se produzca, por la sencilla razón de que Exception es el tipo de error más genérico que puede haber, por tanto se adecua a cualquier error.

Nota:
Realmente el tipo Exception es la clase de la que se derivan (o en la que se basan) todas las clases que manejan algún tipo de excepción o error.
Si te fijas, verás que todos los tipos de excepciones que podemos usar con Catch, terminan con la palabra Exception, esto, además de ser una "norma" o recomendación nos sirve para saber que ese objeto es válido para su uso con Catch. Esto lo deberíamos tener en cuenta cuando avancemos en nuestro aprendizaje y sepamos crear nuestras propias excepciones.

Por otro lado, si sólo usamos tipos específicos de excepciones y se produce un error que no es adecuado a los tipos que queremos interceptar, se producirá una excepción "no interceptada" y el programa finalizará.

Para poder comprobarlo, puedes usar el siguiente código y si simplemente pulsas intro, sin escribir nada o escribes algo que no sea un número, se producirá un error que no está detectado por ninguno de los Catch:

Dim i, j As Integer
Dim s As String
'
Try
    Console.Write("Escribe un número (y pulsa Intro) ")
    s = Console.ReadLine
    i = CInt(s)
    Console.Write("Escribe otro número ")
    s = Console.ReadLine
    j = CInt(s)
    '
    Console.WriteLine("El resultado de dividir {0} por {1} es {2}", i, j, i \ j)
    '
Catch ex As DivideByZeroException
    Console.WriteLine("ERROR: división por cero")
Catch ex As OverflowException
    Console.WriteLine("ERROR: de desbordamiento (número demasiado grande)")
End Try

Sabiendo esto, mi recomendación es que siempre uses un "capturador" genérico de errores, es decir un bloque Catch con el tipo de excepción genérica: Catch variable As Exception.

Después de Catch puedes usar el nombre de variable que quieras, la recomendada es ex, que por cierto, es el nombre que de forma predeterminada muestra el Visual Studio .NET 2003.
Cuando escribimos Try y pulsamos Intro, en la versión 2002 (la primera versión definitiva de Visual Studio .NET), cuando escribimos Try, sólo se muestra el bloque Try y el final: End Try junto a un aviso (dientes de sierra) de que Try necesita un bloque Catch o Finally, pero en la futura versión de VS .NET (futura en la fecha en que escribo esta entrega, ya que ahora mismo sólo es una beta), cuando se escribe Try y se pulsa Intro, además del End Try, se muestra el bloque Catc ex As Exception.

Bien, esto no se ha acabado aún, ya que hay más cosas que contar sobre Try... Catch, como por ejemplo:
- que podemos "lanzar" excepciones, sin necesidad de que se produzca una explícitamente,
- o cómo se comporta el Visual Basic .NET cuando ejecutamos un código que produce error y no hay un bloque específico de tratamiento para ese error, pero existe un bloque Try que aunque esté en otra parte del código sigue activo,
- o que podemos crear nuestras propios tipos de excepciones, pero eso lo veremos cuando tratemos el tema de las clases un poco más a fondo.

Ahora empezaremos viendo
cómo hacer que se produzca una excepción:

Para lanzar (o crear) excepciones tendremos que usar la instrucción Throw seguida de un objeto derivado del tipo Exception.
Normalmente se hace de la siguiente forma:

Throw New Exception("Esto es un error personalizado")

Con este código estamos indicándole al Visual Basic .NET que queremos "lanzar" (Throw) una nueva excepción (New Exception) y que el mensaje que se mostrará, será el que le indiquemos dentro de los paréntesis.

Cuando nosotros lanzamos (o creamos) una excepción, el error lo interceptará un bloque Try (o una instrucción On Error) que esté activo, si no hay ningún bloque Try activo, será el CLR (runtime de .NET Framework) el que se encargará de interceptar esa excepción, pero deteniendo la ejecución del programa... y esto último me recuerda que tengo que explicarte lo que ocurre cuando hay un bloque Try "activo"...

Supongamos que tenemos un bloque Try desde el cual llamamos a algún procedimiento (Sub, Function, etc.), que puede que a su vez llame a otro procedimiento y resulta que en alguno de esos procedimientos se produce una excepción "no controlada", por ejemplo, si tenemos el siguiente código:

Sub Main()
    Dim n, m As Integer
    '
    Try
        n = 10
        m = 15
        Dim k As Integer = n + m
        '
        ' llamanos a un procedimiento
        Prueba()
        '
        '... más código...
        '
        '
    Catch ex As DivideByZeroException
        Console.WriteLine("ERROR: división por cero")
    Catch ex As OverflowException
        Console.WriteLine("ERROR: de desbordamiento (número demasiado grande)")
    Catch ex As InvalidCastException
        Console.WriteLine("ERROR: lo escrito no es un número.")
    Catch ex As Exception
        Console.WriteLine("Se ha producido el error: {0} {1} {2}", ex.Message, vbCrLf, ex.ToString)
    End Try
    '
    Console.WriteLine("Pulsa Intro para terminar")
    Console.ReadLine()
End Sub

Sub Prueba()
    Dim i, j As Integer
    Dim s As String
    Console.Write("Escribe un número (y pulsa Intro) ")
    s = Console.ReadLine
    i = CInt(s)
    Console.Write("Escribe otro número ")
    s = Console.ReadLine
    j = CInt(s)
    Console.WriteLine("El resultado de dividir {0} por {1} es {2}", i, j, i \ j)
End Sub

En el procedimiento Main tenemos cierto código, en el que hemos usado un bloque Try... Catch, dentro del bloque Try además de otras cosas, llamamos al procedimiento Prueba, en el cual se piden dos números y se realizan unas operaciones con esos números, pero en ese procedimiento no tenemos ningún bloque Try que pueda "interceptar" errores.
Tal como vimos anteriormente, si simplemente pulsamos Intro cuando nos pide alguno de esos números o cuando escribimos en el segundo un cero o si alguno de esos dos números que introducimos es más grande que lo que un tipo Integer pude soportar, se producirá un error (o excepción), pero, resulta que dentro del procedimiento Prueba no tenemos nada que "intercepte" los posibles errores que se puedan producir. Si ese código fuese el único que tuviéramos en el programa y se produjera una excepción, sería el CLR o runtime del .NET Framework el que se encargaría de avisarnos de que algo va mal (deteniendo la ejecución del programa). Pero, cuando se llama al procedimiento Prueba desde Main, hay un bloque Try "activo" y el CLR se da cuenta de ese detalle y en lugar de detener el programa y mostrar el error, lo que hace es "pasar" esa excepción a dicho bloque Try (porque está activo) y "confiar" que el error sea "tratado" por dicho bloque.

¿Te enteras?
¿No?
Pues no te preocupes, que algún día puede que te resulte claro... je, je... por ahora, simplemente compruébalo y experimenta... y sigue leyendo, que aún queda un poco más...

A ver si con este otro ejemplo lo entiendes mejor.
Es parecido al anterior, pero con un "pequeño" detalle que lo diferencia:

Sub Main()
    Dim n, m As Integer
    '
    Try
        n = 10
        m = 15
        Dim k As Integer = n + m
        '
        '... más código...
        '
        '
    Catch ex As DivideByZeroException
        Console.WriteLine("ERROR: división por cero")
    Catch ex As OverflowException
        Console.WriteLine("ERROR: de desbordamiento (número demasiado grande)")
    Catch ex As InvalidCastException
        Console.WriteLine("ERROR: lo escrito no es un número.")
    Catch ex As Exception
        Console.WriteLine("Se ha producido el error: {0} {1} {2}", ex.Message, vbCrLf, ex.ToString)
    End Try
    '
    ' llamanos a un procedimiento
    Prueba()
    '
    '
    Console.WriteLine("Pulsa Intro para terminar")
    Console.ReadLine()
End Sub

Sub Prueba()
    Dim i, j As Integer
    Dim s As String
    Console.Write("Escribe un número (y pulsa Intro) ")
    s = Console.ReadLine
    i = CInt(s)
    Console.Write("Escribe otro número ")
    s = Console.ReadLine
    j = CInt(s)
    Console.WriteLine("El resultado de dividir {0} por {1} es {2}", i, j, i \ j)
End Sub

El código del procedimiento Prueba es exactamente igual que el anterior, pero en el procedimiento Main, la llamada a dicho procedimiento no está "dentro" del bloque Try; por tanto si ejecutas el código y cuando te pida alguno de esos dos números escribes algo "incorrecto", se producirá, como bien supones, una excepción, pero en esta ocasión no hay ningún bloque Try que intercepte ese error, por tanto, será el CLR el que se encargue de avisarnos de que algo va mal, pero el CLR nos lo dice de una forma "brusca" y poco educada... ya que detiene el programa y se acabó lo que se daba...

Pues eso... no quiero parecer "mal educado", pero..., se acabó lo que se daba y hasta aquí hemos llegado en esta octava entrega dedicada exclusivamente al tratamiento de errores... al final no he tratado de los arrays... esperemos que sea en la próxima entrega... cosa que descubrirás en cuanto esté publicada, así que, ahora dedícate a "indagar" por tu cuenta y riesgo y sigue probando con esto de las excepciones y los bloques Try... Catch.


Nos vemos.
Guillermo
Nerja, 9~11 de Diciembre de 2002


ir a la entrega anterior ir al índice del curso vb.NET
 
ir al Glosario .NET
ir a la siguiente entrega (o al índice si esta es la última)

la Luna del Guille o... el Guille que está en la Luna... tanto monta...