Uso de RAISERROR desde SQL y gestión de errores desde ADO.NET

Existen multitud de formas de manipular los errores o excepciones desde llamadas en código administrado a procedimientos almacenados (SP en adelante) a través de ADO.NET. Quizás la peculiaridad reside en la responsabilidad que le otorguemos a los SPs.


Desde la versión SQL Server  2005, y con el uso de TRY…CATCH podemos capturar y tratar un error de la siguiente forma.

BEGIN TRY

END TRY
BEGIN CATCH
 

END CATCH


Sin embargo la no debemos olvidar el cómo devolvemos/notificamos un error desde el TRY…CATCH a la llamada realizada desde código administrado.


Devolver el error como resultado


Una opción sería la de retornar la descripción de las propiedades del error producido como resultado como sigue:

BEGIN TRY

END TRY
BEGIN CATCH

SELECT  ERROR_NUMBER()  AS ErrorNumber,     
      ERROR_SEVERITY()  AS ErrorSeverity,
      ERROR_STATE()     AS ErrorState,
      ERROR_PROCEDURE() AS ErrorProcedure,
      ERROR_LINE()      AS ErrorLine,
      ERROR_MESSAGE()   AS ErrorMessage;
END CATCH

Esta forma devolverá una fila de 6 columnas. El principal problema reside en que para la clase que realiza la llamada al SP le constará como que se ha ejecutado correctamente. Además si la llamada se realiza mediante un DbCommand mediante ExecuteScalar únicamente se obtendrá el número de error (primera columna). Por otro lado si el objetivo del SP es el de insertar, eliminar o modificar un conjunto de datos lo único que deberíamos esperar del SP es que nos dijera, en el caso que ha ido correcto, el número de filas afectadas, si se da el caso. En mi opinión, esta es una mala opción y se debería obviar. Además su detección desde la aplicación cliente es confusa.


Devolver el error como mensaje y generar excepción


La opción más correcta elegante es mediante el uso de RAISERROR dentro del CATCH. Sin embargo, para comprender el funcionamiento de esta función deberíamos, en primer lugar, conocer la anatomía de un error en SQL Server.


Anatomía de un error


La información que obtenemos a través del SQL Server Management Studio, en forma de texto es la interpretación que  éste hace de la información enviada por SQL Server a través de varios componentes que establecen sus propiedades.


Message number – Cada error tiene un número identificativo. Los mensajes producidos por el usuario a través de RAISERROR tienen un valor por defecto de 50000. A partir de 50001 en adelante se pueden configurar mensajes de error específicos en sys.messages.

USE master
GO 
EXEC sp_addmessage @msgnum = 600001,
      @severity = 15,
      @msgtext =  ‘Indicated time %s is not allowed.
                        Transports services are only available from
                        09:00AM until 05:00PM.’
      , @lang =         ‘us_english’
      , @with_log =     ‘FALSE’
GO 
EXEC sp_addmessage @msgnum = 600001,
      @severity = 15,
      @msgtext =  ‘La hora %s indicada no es válida.
                        Los servicios únicamente estan disponibles
                        desde las 9h hasta las 17h.’
      , @lang =         ‘Spanish’
      , @with_log =     ‘FALSE’
GO 

Severity level – Importante pues determinará el comportamiento del sistema en base a la severidad del error.



































TABLA 1: RAISERROR Severity Categories
Severity Range Error Number Info @@ERROR Logged As
1 hasta 9 In black NOT SET Informational
10 Not provided NOT SET Informational
11 hasta 14 In red SET Informational
15 In red SET Warning
16 In red SET Error
 

State – Los valores son de 1 a 127.  Se sabe que cada mensaje tiene un estado determinado pero Microsoft no lo ha documentado. Un estado 127 significa que el cliente se ha desconectado sin embargo esa información es trivial puesto que una severidad superior a 18 implica el mismo comportamiento nosotros.


Procedure – En qué objeto (contenedor del T-SQL) se ha producido.


Line – La línea en que se ha producido.


Message text – La descripción del error o mensaje.


Volvamos a la captura del error pero ahora utilizando RAISERROR:

BEGIN TRY     
   SELECT 1/0
END TRY
BEGIN CATCH

— Snippet de los BOL de SQL Server.
DECLARE @ErrorMessage NVARCHAR(4000);
DECLARE @ErrorSeverity INT;
DECLARE @ErrorState INT; 
SELECT     @ErrorMessage = ERROR_MESSAGE(),
    @ErrorSeverity = ERROR_SEVERITY(),
    @ErrorState = ERROR_STATE(); 
RAISERROR (@ErrorMessage, — Message text.
           @ErrorSeverity, — Severity.
           @ErrorState — State.
           ); 
END CATCH

Este es un ejemplo de control de error no esperado mediante RAISERROR. La peculiaridad reside que en desconocemos la severidad del mismo. Según la Tabal 1, si la severidad es inferior o igual a 10, desde la llamada cliente NO se producirá ninguna Excepcion. Sin embargo si es superior sí; ejemplo:

try
{   
   //lanzamos el SP
   
   res = dbCommand.ExecuteNonQuery();
}
catch (SqlException sqlex)
{   
   
//la severidad es superior a 10, luego entra aquí.
}
catch (Exception ex)
{
    //otra excepción no controlada a nivel de aplicación.
} 

El problema está que ante un error de severidad leve (10), como por ejemplo, un error 718 producido por:

708         No hay suficiente espacio de direcciones virtuales en el servidor o no hay suficiente memoria virtual en el equipo. Se ha usado la memoria reservada %1! veces desde el inicio. Cancele la consulta y vuelva a ejecutarla, reduzca la carga del servidor o cancele otras aplicaciones.

El SP se ejecutará correctamente sin embargo la única forma de obtener ese mensaje desde  ADO.NET es suscribiéndonos al evento InfoMessage del objeto DbConnection.

conexion.InfoMessageConnection += new System.Data.SqlClient.SqlInfoMessageEventHandler
                                                          (conexion_InfoMessageConnection);
 void conexion_InfoMessageConnection(object sender, System.Data.SqlClient.SqlInfoMessageEventArgs e){
{
      //aquí obtenemos el mensaje
      string res = e.Message;
} 

Para los errores de severidad 19 o mayor se exige que:


          En la llamada a RAISERROR se especifique la opción WITH LOG.


          Los autores pertenecen la función sysadmin.


Un ejemplo:


Esta  sentencia no dará error pero no guardará LOG en el visor de sucesos.


RAISERROR(‘esto es una prueba’,15,217)


Esta otra guardará una entrada en el visor de sucesos como Warning (TABLA 1)


RAISERROR(‘esto es una prueba’,15,217) WITH LOG


Esta es idéntica a las anteriores pero se procesa como Error no como Warning.


RAISERROR(‘esto es una prueba’,16,217)


RAISERROR(‘esto es una prueba’,16,217) WITH LOG


La siguiente sentencia NO funcionará:


RAISERROR(‘esto es una prueba’,19,217)

Sin embargo la siguiente si

RAISERROR(‘esto es una prueba’,19,217) WITH LOG


Los resultados en el visor de sucesos:

Error      08/05/2008 12:09:55       MSSQL$$$$$$$$$            17063    (2)
Advertencia       08/05/2008 12:05:46       MSSQL$$$$$$$$$            17063    (2)
Error      08/05/2008 12:05:12       MSSQL$$$$$$$$$            17063    (2)


 

5 comentarios en “Uso de RAISERROR desde SQL y gestión de errores desde ADO.NET”

  1. espinete, el primer insert ni siquiera se ejecutará y el segundo al generarse un error de sintaxis en tiempo de ejecución ni siquiera se compila y por ello cancela la transacción, si pruebas: INSERT INTO Test VALUES (999999999999999) te generará un error de overflow pero no cancelará la transacción ya que el error lo propagará el motor de base dedatos…

  2. Desde el analizador ejecuta Ctrl+L para mostrar el plan de ejecución. La primera no te la mostrará puesto que ni siquiera de compila, la segunda si, porque la sintaxis es correcta. otra cosa es que a posteriori la sentencia genere un error de conversion.

    saludos!

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *