|
Detailed error reporting from stored procedures can be extremely
valuable when debugging problems in your application. I will share different
levels of detail that you can build into your stored procedures. You
can choose the level of detail appropriate for your situation.
Let's get right to the examples. (Note that all examples use
Northwind tables and that we're focusing here on errors, so all tests
are for failure cases.)
Compatibility: All code works on both SQL Server 2000 and
SQL Server 2005 Beta 2. It does not leverage any SQL Server 2005
features.
Level Zero - No Detection, No Reporting
CREATE PROCEDURE dbo.CreateRegion0 (
@RegionID
int,
@RegionDescription nchar(50)
) AS BEGIN
SET NOCOUNT ON
INSERT INTO dbo.Region (
RegionID,
RegionDescription
)
VALUES (
@RegionID,
@RegionDescription
)
END
GO |
Procedure CreateRegion0 includes no error detection or reporting.
If we execute this with a RegionID that already exists,
DECLARE @rtnVal int
EXEC @rtnVal = dbo.CreateRegion0
@RegionID
= 2,
@RegionDescription = N'Western'
we get this error
Server: Msg 2627,
Level 14, State 1, Procedure SPTest_CreateRegion0, Line 9
Violation of PRIMARY KEY constraint 'PK_Region'. Cannot insert
duplicate key in object 'Region'.
The statement has been terminated.
and @rtnVal is -4. The error from SQL Server is quite helpful
but does not give any details.
Level One - Error Detection with Minimal Reporting
CREATE PROCEDURE dbo.CreateRegion1 (
@RegionID
int,
@RegionDescription nchar(50)
) AS BEGIN
SET NOCOUNT ON
DECLARE @error int
DECLARE @rowcount int
INSERT INTO dbo.Region (
RegionID,
RegionDescription
)
VALUES (
@RegionID,
@RegionDescription
)
SELECT
@error = @@error,
@rowcount = @@rowcount
IF @error <> 0 BEGIN
RAISERROR('Error %d inserting into Region',
10, 1, @error)
RETURN(@error)
END
ELSE IF @rowcount <> 1 BEGIN
RAISERROR('Rowcount %d inserting into
Region', 10, 1, @rowcount)
WITH SETERROR
RETURN(@@error)
END
RETURN(0)
END
GO |
Procedure CreateRegion1 includes error detection and minimal
reporting through in-line RAISERROR calls. If we execute this with a
RegionID that already exists,
DECLARE @rtnVal int
EXEC @rtnVal = dbo.CreateRegion1
@RegionID
= 2,
@RegionDescription = N'Western'
we get this error
Server: Msg 2627,
Level 14, State 1, Procedure SPTest_CreateRegion1, Line 12
Violation of PRIMARY KEY constraint 'PK_Region'. Cannot insert
duplicate key in object 'Region'.
The statement has been terminated.
Error 2627 inserting into Region
and @rtnVal is 2627. Here, there are two error messages,
but the "custom" message doesn't give us much more information. However,
the return value is much more useful than -4.
Some would argue that the @@rowcount check adds no value.
I disagree, especially in cases where you use "INSERT
INTO ... SELECT ... FROM ...". If you expect to insert
exactly one row and the SELECT returns some number of rows other than
one, the caller needs to know that the insert did not happen.
Could you leave the @@rowcount check out sometimes? Sure.
I never do.
Level Two - User-Defined Messages, More-Detailed
Reporting
sp_addmessage 2000000001, 10, N'Error in %s: Error
%d inserting into %s. %s', US_ENGLISH, FALSE, REPLACE
GO
sp_addmessage 2000000002, 10, N'Error in %s: Insert into %s returned
%d rows. %s', US_ENGLISH, FALSE, REPLACE
GO
CREATE PROCEDURE dbo.CreateRegion2 (
@RegionID
int,
@RegionDescription nchar(50)
) AS BEGIN
-- Error detection and reporting
SET NOCOUNT ON
DECLARE @error int
DECLARE @errorMsg nvarchar(512)
DECLARE @rowcount int
DECLARE @spName sysname
SELECT @spName = Object_Name(@@ProcID)
INSERT INTO dbo.Region (
RegionID,
RegionDescription
)
VALUES (
@RegionID,
@RegionDescription
)
SELECT
@error = @@error,
@rowcount = @@rowcount
IF @error <> 0 BEGIN
EXEC dbo.TagValueList @errorMsg OUTPUT,
N'RegionID', @RegionID,
N'RegionDescription', @RegionDescription
RAISERROR(2000000001, 10, 1, @spName,
@error, 'Region', @errorMsg)
RETURN(@error)
END
ELSE IF @rowcount <> 1 BEGIN
EXEC dbo.TagValueList @errorMsg OUTPUT,
N'RegionID', @RegionID,
N'RegionDescription', @RegionDescription
RAISERROR(2000000002, 10, 1, @spName,
'Region', @rowcount, @errorMsg)
WITH SETERROR
RETURN(@@error)
END
RETURN(0)
END
GO |
Procedure CreateRegion2 uses user-defined messages and more-detailed
reporting.
This procedure uses a special utility procedure (TagValueList)
that creates a tag-value list for the error message. This list can be
used to give the details that will help you debug the problem when it
occurs. From the example code, a call to this procedure creates this
string:
(RegionID: 2) (RegionDescription: Western)
This allows you to log exactly what failed, not just that
a failure happened. Another important feature of this procedure is the
ability to see which parameters or local variables are NULL. In the
second test of CreateRegion2, @RegionDescription is set to NULL. This
is the tag-value string returned:
(RegionID: 2) (RegionDescription: NULL)
After calling CreateRegion2,
DECLARE @rtnVal int
EXEC @rtnVal = dbo.CreateRegion2
@RegionID
= 2,
@RegionDescription = N'Western'
we get this error:
Server: Msg 2627,
Level 14, State 1, Procedure CreateRegion2, Line 16
Violation of PRIMARY KEY constraint 'PK_Region'. Cannot insert
duplicate key in object 'Region'.
The statement has been terminated.
Error in CreateRegion2: Error 2627 inserting into Region. (RegionID:
2) (RegionDescription: Western)
and @rtnVal is 2627. Using a different test (where @RegionDescription
is set to NULL),
DECLARE @rtnVal int
EXEC @rtnVal = dbo.CreateRegion2
@RegionID
= 2,
@RegionDescription = NULL
we get this error:
Server: Msg 515,
Level 16, State 2, Procedure CreateRegion2, Line 16
Cannot insert the value NULL into column 'RegionDescription',
table 'Northwind.dbo.Region'; column does not allow nulls. INSERT
fails.
The statement has been terminated.
Error in CreateRegion2: Error 515 inserting into Region. (RegionID:
2) (RegionDescription: NULL)
and @rtnVal is 515.
Here, the results are better than in level one because of
the stored procedure name and tag-value pair details in the RAISERROR
output.
Level Three - User-Defined Messages, Failure
Reasons
sp_addmessage 2000000003, 10, N'Error in %s: Failed creating
Region record because this RegionID already exists. %s', US_ENGLISH,
FALSE, REPLACE
GO
CREATE PROCEDURE CreateRegion3 (
@RegionID
int,
@RegionDescription nchar(50)
) AS BEGIN
-- Full error detection and reporting
SET NOCOUNT ON
DECLARE @error int
DECLARE @errorMsg nvarchar(512)
DECLARE @rowcount int
DECLARE @spName sysname
SELECT @spName = Object_Name(@@ProcID)
INSERT INTO dbo.Region (
RegionID,
RegionDescription
)
VALUES (
@RegionID,
@RegionDescription
)
SELECT
@error = @@error,
@rowcount = @@rowcount
IF @error <> 0 BEGIN
EXEC dbo.TagValueList @errorMsg OUTPUT,
N'RegionID', @RegionID,
N'RegionDescription', @RegionDescription
-- Error 2627 indicates failure of a
primary key constraint
IF @error = 2627
AND EXISTS (
SELECT 1
FROM dbo.Region
WHERE RegionID
= @RegionID
) BEGIN
RAISERROR(2000000003,
10, 1, @spName, @errorMsg) WITH SETERROR
RETURN(@@error)
END
ELSE BEGIN
RAISERROR(2000000001,
10, 1, @spName, @error, 'Region', @errorMsg)
RETURN(@error)
END
END
ELSE IF @rowcount <> 1 BEGIN
EXEC dbo.TagValueList @errorMsg OUTPUT,
N'RegionID', @RegionID,
N'RegionDescription', @RegionDescription
RAISERROR(2000000002, 10, 1, @spName,
'Region', @rowcount, @errorMsg)
WITH SETERROR
RETURN(@@error)
END
RETURN(0)
END
GO |
Procedure CreateRegion3 includes user-defined messages and
very detailed reporting plus the ability to report on specific reasons
for failure. Instead of raising an error stating that an error occurred
inserting into Region, this procedure checks for a specific error that
is expected (primary key failure). If that is the error, then it raises
an error with details about that error. If that is not the error, then
it raises a more general error but still supplies as much detail as
possible.
After calling CreateRegion3, we get this error:
Server: Msg 2627,
Level 14, State 1, Procedure CreateRegion3, Line 16
Violation of PRIMARY KEY constraint 'PK_Region'. Cannot insert
duplicate key in object 'Region'.
The statement has been terminated.
Error in CreateRegion3: Failed creating Region record because this
RegionID already exists. (RegionID: 2) (RegionDescription: Western)
and @rtnVal is 2000000003. Here, the results are better than
in level two because the RAISERROR output gives details about
which error occurred.
The time when this is most valuable is when a table has multiple
constraints. For instance, if an insert into Orders failed, it could
be because of an invalid CustomerID, EmployeeID, or ShipperID (ShipVia).
All three of these would cause an @@error value of 547. Now look at
ErrorReportingOrders. This script includes
a level-three implementation of CreateOrder. Here, if @@error is 547
(foreign key violation), then each possible cause is tested. If none
of those cases is true, then a more generic error is raised.
Code that calls CreateOrder can include this type of logic:
If <returnValue>
= 2000000010, the CustomerID was invalid
Else if <returnValue> = 2000000011, the EmployeeID was invalid
Else if <returnValue> = 2000000012, the ShipperID was invalid
Else if <returnValue> = 2000000001, an error occurred on INSERT
Else if <returnValue> = 2000000002, the INSERT failed and rowcount
was not 1
Else unexpected error
In many cases, the calling code does not need such detailed
logic. But when it is needed, this allows very detailed information
to be passed to the calling code.
You won't choose to use level three code in every case, but
when you need it, it will help you to quickly find the source of the
problem. I recommend that you use either level two or three. It may
take more coding up-front, but when something goes wrong in production,
you will be very glad that you took the time during development to include
full error detection and reporting.
Watch for "Detecting and Reporting Errors in Stored Procedures
- Part 2: SQL Server 2005 TRY-CATCH Blocks".
Appendix
TagValueList Utility
Procedure
This is a utility procedure for use in error reporting.
CREATE PROCEDURE dbo.TagValueList (
@List varchar(512)
OUTPUT,
@Tag1 varchar(64) = NULL,
@Value1 varchar(64) = NULL,
@Tag2 varchar(64) = NULL,
@Value2 varchar(64) = NULL,
@Tag3 varchar(64) = NULL,
@Value3 varchar(64) = NULL,
@Tag4 varchar(64) = NULL,
@Value4 varchar(64) = NULL,
@Tag5 varchar(64) = NULL,
@Value5 varchar(64) = NULL,
@Tag6 varchar(64) = NULL,
@Value6 varchar(64) = NULL,
@Tag7 varchar(64) = NULL,
@Value7 varchar(64) = NULL,
@Tag8 varchar(64) = NULL,
@Value8 varchar(64) = NULL,
@Tag9 varchar(64) = NULL,
@Value9 varchar(64) = NULL,
@Tag10 varchar(64) = NULL,
@Value10 varchar(64) = NULL,
@Tag11 varchar(64) = NULL,
@Value11 varchar(64) = NULL,
@Tag12 varchar(64) = NULL,
@Value12 varchar(64) = NULL,
@Tag13 varchar(64) = NULL,
@Value13 varchar(64) = NULL,
@Tag14 varchar(64) = NULL,
@Value14 varchar(64) = NULL,
@Tag15 varchar(64) = NULL,
@Value15 varchar(64) = NULL,
@Tag16 varchar(64) = NULL,
@Value16 varchar(64) = NULL,
@Tag17 varchar(64) = NULL,
@Value17 varchar(64) = NULL,
@Tag18 varchar(64) = NULL,
@Value18 varchar(64) = NULL,
@Tag19 varchar(64) = NULL,
@Value19 varchar(64) = NULL,
@Tag20 varchar(64) = NULL,
@Value20 varchar(64) = NULL
) AS BEGIN
-- Called to build up a tag/value pair string. Normally used
for error messages.
-- Pass in as many Tag/Value pairs as you want to see in @List
(up to twenty pairs).
-- Tag/Value 1
IF @Tag1 IS NULL BEGIN
RETURN(0)
END
-- This line begins the string
SELECT @List = '(' + @Tag1 + ': ' + ISNULL(RTRIM(@Value1), 'NULL')
+ ')'
-- Tag/Value 2
IF @Tag2 IS NULL BEGIN
RETURN(0)
END
SELECT @List = @List + ' (' + @Tag2 + ': ' + ISNULL(RTRIM(@Value2),
'NULL') + ')'
-- Tag/Value 3
IF @Tag3 IS NULL BEGIN
RETURN(0)
END
SELECT @List = @List + ' (' + @Tag3 + ': ' + ISNULL(RTRIM(@Value3),
'NULL') + ')'
-- Tag/Value 4
IF @Tag4 IS NULL BEGIN
RETURN(0)
END
SELECT @List = @List + ' (' + @Tag4 + ': ' + ISNULL(RTRIM(@Value4),
'NULL') + ')'
-- Tag/Value 5
IF @Tag5 IS NULL BEGIN
RETURN(0)
END
SELECT @List = @List + ' (' + @Tag5 + ': ' + ISNULL(RTRIM(@Value5),
'NULL') + ')'
-- Tag/Value 6
IF @Tag6 IS NULL BEGIN
RETURN(0)
END
SELECT @List = @List + ' (' + @Tag6 + ': ' + ISNULL(RTRIM(@Value6),
'NULL') + ')'
-- Tag/Value 7
IF @Tag7 IS NULL BEGIN
RETURN(0)
END
SELECT @List = @List + ' (' + @Tag7 + ': ' + ISNULL(RTRIM(@Value7),
'NULL') + ')'
-- Tag/Value 8
IF @Tag8 IS NULL BEGIN
RETURN(0)
END
SELECT @List = @List + ' (' + @Tag8 + ': ' + ISNULL(RTRIM(@Value8),
'NULL') + ')'
-- Tag/Value 9
IF @Tag9 IS NULL BEGIN
RETURN(0)
END
SELECT @List = @List + ' (' + @Tag9 + ': ' + ISNULL(RTRIM(@Value9),
'NULL') + ')'
-- Tag/Value 10
IF @Tag10 IS NULL BEGIN
RETURN(0)
END
SELECT @List = @List + ' (' + @Tag10 + ': ' + ISNULL(RTRIM(@Value10),
'NULL') + ')'
-- Tag/Value 11
IF @Tag11 IS NULL BEGIN
RETURN(0)
END
SELECT @List = @List + ' (' + @Tag11 + ': ' + ISNULL(RTRIM(@Value11),
'NULL') + ')'
-- Tag/Value 12
IF @Tag12 IS NULL BEGIN
RETURN(0)
END
SELECT @List = @List + ' (' + @Tag12 + ': ' + ISNULL(RTRIM(@Value12),
'NULL') + ')'
-- Tag/Value 13
IF @Tag13 IS NULL BEGIN
RETURN(0)
END
SELECT @List = @List + ' (' + @Tag13 + ': ' + ISNULL(RTRIM(@Value13),
'NULL') + ')'
-- Tag/Value 14
IF @Tag14 IS NULL BEGIN
RETURN(0)
END
SELECT @List = @List + ' (' + @Tag14 + ': ' + ISNULL(RTRIM(@Value14),
'NULL') + ')'
-- Tag/Value 15
IF @Tag15 IS NULL BEGIN
RETURN(0)
END
SELECT @List = @List + ' (' + @Tag15 + ': ' + ISNULL(RTRIM(@Value15),
'NULL') + ')'
-- Tag/Value 16
IF @Tag16 IS NULL BEGIN
RETURN(0)
END
SELECT @List = @List + ' (' + @Tag16 + ': ' + ISNULL(RTRIM(@Value16),
'NULL') + ')'
-- Tag/Value 17
IF @Tag17 IS NULL BEGIN
RETURN(0)
END
SELECT @List = @List + ' (' + @Tag17 + ': ' + ISNULL(RTRIM(@Value17),
'NULL') + ')'
-- Tag/Value 18
IF @Tag18 IS NULL BEGIN
RETURN(0)
END
SELECT @List = @List + ' (' + @Tag18 + ': ' + ISNULL(RTRIM(@Value18),
'NULL') + ')'
-- Tag/Value 19
IF @Tag19 IS NULL BEGIN
RETURN(0)
END
SELECT @List = @List + ' (' + @Tag19 + ': ' + ISNULL(RTRIM(@Value19),
'NULL') + ')'
-- Tag/Value 20
IF @Tag20 IS NULL BEGIN
RETURN(0)
END
SELECT @List = @List + ' (' + @Tag20 + ': ' + ISNULL(RTRIM(@Value20),
'NULL') + ')'
RETURN(0)
END
GO |
CreateOrder Procedure
and Tests
sp_addmessage 2000000001, 10, N'Error in %s: Error
%d inserting into %s. %s', US_ENGLISH, FALSE, REPLACE
GO
sp_addmessage 2000000002, 10, N'Error in %s: Insert into %s returned
%d rows. %s', US_ENGLISH, FALSE, REPLACE
GO
sp_addmessage 2000000010, 10, N'Error in %s: Failed creating Orders
record because the CustomerID supplied is not valid. %s', US_ENGLISH,
FALSE, REPLACE
GO
sp_addmessage 2000000011, 10, N'Error in %s: Failed creating Orders
record because the EmployeeID supplied is not valid. %s', US_ENGLISH,
FALSE, REPLACE
GO
sp_addmessage 2000000012, 10, N'Error in %s: Failed creating Orders
record because the ShipperID supplied is not valid. %s', US_ENGLISH,
FALSE, REPLACE
GO
CREATE PROCEDURE dbo.CreateOrder (
@CustomerID nchar(5),
@EmployeeID int,
@ShipperID int,
@OrderID int
OUTPUT
) AS BEGIN
-- Full error detection and reporting (multiple FKs)
SET NOCOUNT ON
DECLARE @error int
DECLARE @errorMsg nvarchar(512)
DECLARE @rowcount int
DECLARE @spName sysname
SELECT @spName = Object_Name(@@ProcID)
INSERT INTO dbo.Orders (
CustomerID,
EmployeeID,
ShipVia,
OrderDate
)
VALUES (
@CustomerID,
@EmployeeID,
@ShipperID,
GETDATE()
)
SELECT
@error = @@error,
@rowcount = @@rowcount,
@OrderID = SCOPE_IDENTITY()
IF @error <> 0 BEGIN
EXEC TagValueList @errorMsg OUTPUT, N'CustomerID',
@CustomerID,
N'EmployeeID', @EmployeeID, N'ShipperID', @ShipperID
-- Error 547 indicates failure of a foreign
key constraint
IF @error = 547 BEGIN
IF NOT EXISTS
(
SELECT 1
FROM Customers
WHERE CustomerID = @CustomerID
) BEGIN
RAISERROR(2000000010, 10, 1, @spName, @errorMsg) WITH SETERROR
RETURN(@@error)
END
ELSE IF NOT EXISTS
(
SELECT 1
FROM Employees
WHERE EmployeeID = @EmployeeID
) BEGIN
RAISERROR(2000000011, 10, 1, @spName, @errorMsg) WITH SETERROR
RETURN(@@error)
END
ELSE IF NOT EXISTS
(
SELECT 1
FROM Shippers
WHERE ShipperID = @ShipperID
) BEGIN
RAISERROR(2000000012, 10, 1, @spName, @errorMsg) WITH SETERROR
RETURN(@@error)
END
END
ELSE BEGIN
RAISERROR(2000000001,
10, 1, @error, @spName, 'Region', @errorMsg)
RETURN(@error)
END
END
ELSE IF @rowcount <> 1 BEGIN
EXEC TagValueList @errorMsg OUTPUT, N'CustomerID',
@CustomerID,
N'EmployeeID', @EmployeeID, N'ShipperID', @ShipperID
RAISERROR(2000000002, 10, 1, @spName,
'Orders', @rowcount, @errorMsg) WITH SETERROR
RETURN(@@error)
END
RETURN(0)
END
GO
-- ===============================================================================================
-- Tests
-- ===============================================================================================
DECLARE @rtnVal int
DECLARE @rtnMsg varchar(255)
DECLARE @msg varchar(255)
DECLARE @order1 int
PRINT ''
PRINT 'EXEC CreateOrder (invalid CustomerID)'
EXEC @rtnVal = dbo.CreateOrder
@CustomerID = N'xxx',
@EmployeeID = 1,
@ShipperID = 2,
@OrderID = @order1
OUTPUT
SELECT @rtnMsg = 'Return value: ' + CONVERT(varchar, @rtnVal)
PRINT @rtnMsg
PRINT ''
PRINT 'EXEC CreateOrder (invalid EmployeeID)'
EXEC @rtnVal = dbo.CreateOrder
@CustomerID = N'BOTTM',
@EmployeeID = -1,
@ShipperID = 2,
@OrderID = @order1
OUTPUT
SELECT @rtnMsg = 'Return value: ' + CONVERT(varchar, @rtnVal)
PRINT @rtnMsg
PRINT ''
PRINT 'EXEC CreateOrder (invalid ShipperID)'
EXEC @rtnVal = dbo.CreateOrder
@CustomerID = N'BOTTM',
@EmployeeID = 1,
@ShipperID = -1,
@OrderID = @order1
OUTPUT
SELECT @rtnMsg = 'Return value: ' + CONVERT(varchar, @rtnVal)
PRINT @rtnMsg
PRINT ''
PRINT 'EXEC CreateOrder'
EXEC @rtnVal = dbo.CreateOrder
@CustomerID = N'BOTTM',
@EmployeeID = 1,
@ShipperID = 1,
@OrderID = @order1
OUTPUT
SELECT @rtnMsg = 'Return value: ' + CONVERT(varchar, @rtnVal)
PRINT @rtnMsg
IF @rtnVal = 0 BEGIN
SELECT @msg = 'Successfully created order ' + CONVERT(varchar,
@order1)
PRINT @msg
END
GO |
Originally published December 15, 2004 on SqlJunkies.com
|
|
|