164

I'm writing a user-defined function in SQL Server 2008. I know that functions cannot raise errors in the usual way - if you try to include the RAISERROR statement SQL returns:

Msg 443, Level 16, State 14, Procedure ..., Line ...
Invalid use of a side-effecting operator 'RAISERROR' within a function.

But the fact is, the function takes some input, which may be invalid and, if it is, there is no meaningful value the function can return. What do I do then?

I could, of course, return NULL, but it would be difficult for any developer using the function to troubleshoot this. I could also cause a division by zero or something like that - this would generate an error message, but a misleading one. Is there any way I can have my own error message reported somehow?

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
EMP
  • 59,148
  • 53
  • 164
  • 220

9 Answers9

259

You can use CAST to throw meaningful error:

create function dbo.throwError()
returns nvarchar(max)
as
begin
    return cast('Error happened here.' as int);
end

Then Sql Server will show some help information:

Msg 245, Level 16, State 1, Line 1
Conversion failed when converting the varchar value 'Error happened here.' to data type int.
Vladimir Korolev
  • 2,977
  • 1
  • 17
  • 10
  • 143
    Great answer, but JEEZ wotta hack. >:( – JohnL4 Oct 12 '11 at 16:34
  • 5
    For an inline-table-valued-function where the RETURN is a simple select, this alone doesn't work because nothing is returned - not even null, and in my case I wanted to throw an error when nothing was found. I didn't want to break down the inline function into a multi-statment one for obvious performance reasons. Instead I used your solution plus ISNULL and MAX. The RETURN statment now looks like this: SELECT ISNULL(MAX(E.EntityID), CAST('The Lookup (' + @LookupVariable + ') does not exist.' as Int))[EntityID] FROM Entity as E WHERE E.Lookup = @ LookupVariable – MikeTeeVee Mar 03 '12 at 01:06
  • Yes, you can throw an error, but it doesn't seem that you can conditionally throw an error. The function gets executed irrespective of the code path. – satnhak Mar 06 '12 at 12:01
  • 13
    Great solution, but for those that are using a TVF, this can't easily be part of the return. For those: `declare @error int; set @error = 'Error happened here.'; ` – Tim Lehner May 07 '12 at 14:45
  • @MikeTeeVee great point, i made a variation on this below because I couldn't use "MAX" in my particular case. Tagging you just in case you are interested – davec Jun 27 '13 at 22:30
  • 1
    Works awesome in TVF as well if you put this in a CASE statement for a column, or you can make it multi-statement TVF, very cool solution! – Chris Schaller Apr 17 '15 at 05:44
  • 32
    I hate this with the power of a thousand burning suns. No other options? Fine. But cripes... – Remi Despres-Smyth Feb 04 '16 at 13:19
  • This used this trick into a multi-statement TVF (setting the intentional-wrong cast-to-string to a variable). It worked fine when the TVF was invoked (with the parameters that caused the error i wanted to report) in a connection without open transactions. But when the TVF was invoked with those crash parameters another not-related errow was thrown: "The current transaction cannot be committed and cannot support operations that write to the log file". I hope this advice be hepful – Mauricio Ortega Aug 03 '22 at 20:40
22

The usual trick is to force a divide by 0. This will raise an error and interrupt the current statement that is evaluating the function. If the developer or support person knows about this behavior, investigating and troubleshooting the problem is fairly easy as the division by 0 error is understood as a symptom of a different, unrelated problem.

As bad as this looks from any point of view, unfortunately the design of SQL functions at the moment allows no better choice. Using RAISERROR should absolutely be allowed in functions.

Remus Rusanu
  • 288,378
  • 40
  • 442
  • 569
10

Following on from Vladimir Korolev's answer, the idiom to conditionally throw an error is

CREATE FUNCTION [dbo].[Throw]
(
    @error NVARCHAR(MAX)
)
RETURNS BIT
AS
BEGIN
    RETURN CAST(@error AS INT)
END
GO

DECLARE @error NVARCHAR(MAX)
DECLARE @bit BIT

IF `error condition` SET @error = 'My Error'
ELSE SET @error = '0'

SET @bit = [dbo].[Throw](@error)    
satnhak
  • 9,407
  • 5
  • 63
  • 81
8

I think the cleanest way is to just accept that the function can return NULL if invalid arguments are passed. As long is this is clearly documented then this should be okay?

-- =============================================
-- Author: AM
-- Create date: 03/02/2010
-- Description: Returns the appropriate exchange rate
-- based on the input parameters.
-- If the rate cannot be found, returns NULL
-- (RAISEERROR can't be used in UDFs)
-- =============================================
ALTER FUNCTION [dbo].[GetExchangeRate] 
(
    @CurrencyFrom char(3),
    @CurrencyTo char(3),
    @OnDate date
)
RETURNS decimal(18,4)
AS
BEGIN

  DECLARE @ClosingRate as decimal(18,4)

    SELECT TOP 1
        @ClosingRate=ClosingRate
    FROM
        [FactCurrencyRate]
    WHERE
        FromCurrencyCode=@CurrencyFrom AND
        ToCurrencyCode=@CurrencyTo AND
        DateID=dbo.DateToIntegerKey(@OnDate)

    RETURN @ClosingRate 

END
GO
AndyM
  • 3,574
  • 6
  • 39
  • 45
8

A few folks were asking about raising errors in Table-Valued functions, since you can't use "RETURN [invalid cast]" sort of things. Assigning the invalid cast to a variable works just as well.

CREATE FUNCTION fn()
RETURNS @T TABLE (Col CHAR)  
AS
BEGIN

DECLARE @i INT = CAST('booooom!' AS INT)  

RETURN

END

This results in:

Msg 245, Level 16, State 1, Line 14 Conversion failed when converting the varchar value 'booooom!' to data type int.

Musakkhir Sayyed
  • 7,012
  • 13
  • 42
  • 65
NightShovel
  • 3,032
  • 1
  • 31
  • 35
  • This used this trick into a multi-statement TVF (setting the intentional-wrong cast-to-string to a variable). It worked fine when the TVF was invoked (with the parameters that caused the error i wanted to report) in a connection without open transactions. But when the TVF was invoked with those crash parameters another not-related errow was thrown: "The current transaction cannot be committed and cannot support operations that write to the log file". I hope this advice be hepful – Mauricio Ortega Aug 03 '22 at 20:40
7

RAISEERROR or @@ERROR are not allowed in UDFs. Can you turn the UDF into a strored procedure?

From Erland Sommarskog's article Error Handling in SQL Server – a Background:

User-defined functions are usually invoked as part of a SET, SELECT, INSERT, UPDATE or DELETE statement. What I have found is that if an error appears in a multi-statement table-valued function or in a scalar function, the execution of the function is aborted immediately, and so is the statement the function is part of. Execution continues on the next line, unless the error aborted the batch. In either case, @@error is 0. Thus, there is no way to detect that an error occurred in a function from T-SQL.

The problem does not appear with inline table-functions, since an inline table-valued function is basically a macro that the query processor pastes into the query.

You can also execute scalar functions with the EXEC statement. In this case, execution continues if an error occurs (unless it is a batch-aborting error). @@error is set, and you can check the value of @@error within the function. It can be problematic to communicate the error to the caller though.

Mitch Wheat
  • 295,962
  • 43
  • 465
  • 541
6

The top answer is generally best, but does not work for inline table valued functions.

MikeTeeVee gave a solution for this in his comment on the top answer, but it required use of an aggregate function like MAX, which did not work well for my circumstance.

I messed around with an alternate solution for the case where you need an inline table valued udf that returns something like select * instead of an aggregate. Sample code solving this particular case is below. As someone has already pointed out... "JEEZ wotta hack" :) I welcome any better solution for this case!

create table foo (
    ID nvarchar(255),
    Data nvarchar(255)
)
go

insert into foo (ID, Data) values ('Green Eggs', 'Ham')
go

create function dbo.GetFoo(@aID nvarchar(255)) returns table as return (
    select *, 0 as CausesError from foo where ID = @aID

    --error checking code is embedded within this union
    --when the ID exists, this second selection is empty due to where clause at end
    --when ID doesn't exist, invalid cast with case statement conditionally causes an error
    --case statement is very hack-y, but this was the only way I could get the code to compile
    --for an inline TVF
    --simpler approaches were caught at compile time by SQL Server
    union

    select top 1 *, case
                        when ((select top 1 ID from foo where ID = @aID) = @aID) then 0
                        else 'Error in GetFoo() - ID "' + IsNull(@aID, 'null') + '" does not exist'
                    end
    from foo where (not exists (select ID from foo where ID = @aID))
)
go

--this does not cause an error
select * from dbo.GetFoo('Green Eggs')
go

--this does cause an error
select * from dbo.GetFoo('Yellow Eggs')
go

drop function dbo.GetFoo
go

drop table foo
go
davec
  • 323
  • 2
  • 5
  • 14
  • 1
    for anyone reading, i did not look at potential performance effects... i wouldn't be surprised if the hack union + case statement slows things down... – davec Jun 27 '13 at 22:42
4

I can't comment under davec's answer regarding table valued function, but in my humble opinion this is easier solution:

CREATE FUNCTION dbo.ufn_test (@a TINYINT)
RETURNS @returns TABLE(Column1 VARCHAR(10), Value1 TINYINT)
BEGIN
    IF @a>50 -- if @a > 50 - raise an error
    BEGIN
      INSERT INTO @returns (Column1, Value1)
      VALUES('error','@a is bigger than 50!') -- reminder Value1 should be TINYINT
    END

    INSERT INTO @returns (Column1, Value1)
    VALUES('Something',@a)
    RETURN;
END

SELECT Column1, Value1 FROM dbo.ufn_test(1) -- this is okay
SELECT Column1, Value1 FROM dbo.ufn_test(51) -- this will raise an error
  • Your solution works better when error condition known ahead of time & can hard code it into the code. (i.e. @a>50) However it cannot handle dynamic error conditions (e.g. what if later you need more than 50 values in table?) - My original solution does not have a hard coded error condition but has that ugly union - Probably hybrid is best. Check for the error up front with a select not exists and return immediately (or use casting approach of NightShovel's answer to force error). If things look good, then return the actual valid table. Avoid performance hit from union at cost of querying twice – davec Feb 16 '22 at 03:36
-3

One way (a hack) is to have a function/stored procedure that performs an invalid action. For example, the following pseudo SQL

create procedure throw_error ( in err_msg varchar(255))
begin
insert into tbl_throw_error (id, msg) values (null, err_msg);
insert into tbl_throw_error (id, msg) values (null, err_msg);
end;

Where on the table tbl_throw_error, there is a unique constraint on the column err_msg. A side-effect of this (at least on MySQL), is that the value of err_msg is used as the description of the exception when it gets back up into the application level exception object.

I don't know if you can do something similar with SQL Server, but worth a shot.

Alex
  • 2,435
  • 17
  • 18