Sql server обработка ошибок

Привет, Хабр! Представляю вашему вниманию перевод статьи «Error and Transaction Handling in SQL Server. Part One – Jumpstart Error Handling» автора Erland Sommarskog.

1. Введение

Эта статья – первая в серии из трёх статей, посвященных обработке ошибок и транзакций в SQL Server. Её цель – дать вам быстрый старт в теме обработки ошибок, показав базовый пример, который подходит для большей части вашего кода. Эта часть написана в расчете на неопытного читателя, и по этой причине я намеренно умалчиваю о многих деталях. В данный момент задача состоит в том, чтобы рассказать как без упора на почему. Если вы принимаете мои слова на веру, вы можете прочесть только эту часть и отложить остальные две для дальнейших этапов в вашей карьере.

С другой стороны, если вы ставите под сомнение мои рекомендации, вам определенно необходимо прочитать две остальные части, где я погружаюсь в детали намного более глубоко, исследуя очень запутанный мир обработки ошибок и транзакций в SQL Server. Вторая и третья части, так же, как и три приложения, предназначены для читателей с более глубоким опытом. Первая статья — короткая, вторая и третья значительно длиннее.

Все статьи описывают обработку ошибок и транзакций в SQL Server для версии 2005 и более поздних версий.

1.1 Зачем нужна обработка ошибок?

Почему мы обрабатываем ошибки в нашем коде? На это есть много причин. Например, на формах в приложении мы проверяем введенные данные и информируем пользователей о допущенных при вводе ошибках. Ошибки пользователя – это предвиденные ошибки. Но нам также нужно обрабатывать непредвиденные ошибки. То есть, ошибки могут возникнуть из-за того, что мы что-то упустили при написании кода. Простой подход – это прервать выполнение или хотя бы вернуться на этап, в котором мы имеем полный контроль над происходящим. Недостаточно будет просто подчеркнуть, что совершенно непозволительно игнорировать непредвиденные ошибки. Это недостаток, который может вызвать губительные последствия: например, стать причиной того, что приложение будет предоставлять некорректную информацию пользователю или, что еще хуже, сохранять некорректные данные в базе. Также важно сообщать о возникновении ошибки с той целью, чтобы пользователь не думал о том, что операция прошла успешно, в то время как ваш код на самом деле ничего не выполнил.

Мы часто хотим, чтобы в базе данных изменения были атомарными. Например, задача по переводу денег с одного счета на другой. С этой целью мы должны изменить две записи в таблице CashHoldings и добавить две записи в таблицу Transactions. Абсолютно недопустимо, чтобы ошибки или сбой привели к тому, что деньги будут переведены на счет получателя, а со счета отправителя они не будут списаны. По этой причине обработка ошибок также касается и обработки транзакций. В приведенном примере нам нужно обернуть операцию в BEGIN TRANSACTION и COMMIT TRANSACTION, но не только это: в случае ошибки мы должны убедиться, что транзакция откачена.

2. Основные команды

Мы начнем с обзора наиболее важных команд, которые необходимы для обработки ошибок. Во второй части я опишу все команды, относящиеся к обработке ошибок и транзакций.

2.1 TRY-CATCH

Основным механизмом обработки ошибок является конструкция TRY-CATCH, очень напоминающая подобные конструкции в других языках. Структура такова:

BEGIN TRY
   <обычный код>
END TRY
BEGIN CATCH
   <обработка ошибок>
END CATCH

Если какая-либо ошибка появится в <обычный код>, выполнение будет переведено в блок CATCH, и будет выполнен код обработки ошибок.

Как правило, в CATCH откатывают любую открытую транзакцию и повторно вызывают ошибку. Таким образом, вызывающая клиентская программа понимает, что что-то пошло не так. Повторный вызов ошибки мы обсудим позже в этой статье.

Вот очень быстрый пример:

BEGIN TRY
   DECLARE @x int
   SELECT @x = 1/0
   PRINT 'Not reached'
END TRY
BEGIN CATCH 
   PRINT 'This is the error: ' + error_message()
END CATCH

Результат выполнения: This is the error: Divide by zero error encountered.

Мы вернемся к функции error_message() позднее. Стоит отметить, что использование PRINT в обработчике CATCH приводится только в рамках экспериментов и не следует делать так в коде реального приложения.

Если <обычный код> вызывает хранимую процедуру или запускает триггеры, то любая ошибка, которая в них возникнет, передаст выполнение в блок CATCH. Если более точно, то, когда возникает ошибка, SQL Server раскручивает стек до тех пор, пока не найдёт обработчик CATCH. И если такого обработчика нет, SQL Server отправляет сообщение об ошибке напрямую клиенту.

Есть одно очень важное ограничение у конструкции TRY-CATCH, которое нужно знать: она не ловит ошибки компиляции, которые возникают в той же области видимости. Рассмотрим пример:

CREATE PROCEDURE inner_sp AS
   BEGIN TRY
      PRINT 'This prints'
      SELECT * FROM NoSuchTable
      PRINT 'This does not print'
   END TRY
   BEGIN CATCH
      PRINT 'And nor does this print'
   END CATCH
go
EXEC inner_sp

Выходные данные:

This prints
Msg 208, Level 16, State 1, Procedure inner_sp, Line 4
Invalid object name 'NoSuchTable'

Как можно видеть, блок TRY присутствует, но при возникновении ошибки выполнение не передается блоку CATCH, как это ожидалось. Это применимо ко всем ошибкам компиляции, таким как пропуск колонок, некорректные псевдонимы и тому подобное, которые возникают во время выполнения. (Ошибки компиляции могут возникнуть в SQL Server во время выполнения из-за отложенного разрешения имен – особенность, благодаря которой SQL Server позволяет создать процедуру, которая обращается к несуществующим таблицам.)

Эти ошибки не являются полностью неуловимыми; вы не можете поймать их в области, в которой они возникают, но вы можете поймать их во внешней области. Добавим такой код к предыдущему примеру:

CREATE PROCEDURE outer_sp AS
   BEGIN TRY
      EXEC inner_sp
   END TRY
   BEGIN CATCH
      PRINT 'The error message is: ' + error_message()
   END CATCH
go
EXEC outer_sp

Теперь мы получим на выходе это:

This prints
The error message is: Invalid object name 'NoSuchTable'.

На этот раз ошибка была перехвачена, потому что сработал внешний обработчик CATCH.

2.2 SET XACT_ABORT ON

В начало ваших хранимых процедур следует всегда добавлять это выражение:

SET XACT_ABORT, NOCOUNT ON

Оно активирует два параметра сессии, которые выключены по умолчанию в целях совместимости с предыдущими версиями, но опыт доказывает, что лучший подход – это иметь эти параметры всегда включенными. Поведение SQL Server по умолчанию в той ситуации, когда не используется TRY-CATCH, заключается в том, что некоторые ошибки прерывают выполнение и откатывают любые открытые транзакции, в то время как с другими ошибками выполнение последующих инструкций продолжается. Когда вы включаете XACT_ABORT ON, почти все ошибки начинают вызывать одинаковый эффект: любая открытая транзакция откатывается, и выполнение кода прерывается. Есть несколько исключений, среди которых наиболее заметным является выражение RAISERROR.

Параметр XACT_ABORT необходим для более надежной обработки ошибок и транзакций. В частности, при настройках по умолчанию есть несколько ситуаций, когда выполнение может быть прервано без какого-либо отката транзакции, даже если у вас есть TRY-CATCH. Мы видели такой пример в предыдущем разделе, где мы выяснили, что TRY-CATCH не перехватывает ошибки компиляции, возникшие в той же области. Открытая транзакция, которая не была откачена из-за ошибки, может вызвать серьезные проблемы, если приложение работает дальше без завершения транзакции или ее отката.

Для надежной обработки ошибок в SQL Server вам необходимы как TRY-CATCH, так и SET XACT_ABORT ON. Среди них инструкция SET XACT_ABORT ON наиболее важна. Если для кода на промышленной среде только на нее полагаться не стоит, то для быстрых и простых решений она вполне подходит.

Параметр NOCOUNT не имеет к обработке ошибок никакого отношения, но включение его в код является хорошей практикой. NOCOUNT подавляет сообщения вида (1 row(s) affected), которые вы можете видеть в панели Message в SQL Server Management Studio. В то время как эти сообщения могут быть полезны при работе c SSMS, они могут негативно повлиять на производительность в приложении, так как увеличивают сетевой трафик. Сообщение о количестве строк также может привести к ошибке в плохо написанных клиентских приложениях, которые могут подумать, что это данные, которые вернул запрос.

Выше я использовал синтаксис, который немного необычен. Большинство людей написали бы два отдельных выражения:

SET NOCOUNT ON
SET XACT_ABORT ON

Между ними нет никакого отличия. Я предпочитаю версию с SET и запятой, т.к. это снижает уровень шума в коде. Поскольку эти выражения должны появляться во всех ваших хранимых процедурах, они должны занимать как можно меньше места.

3. Основной пример обработки ошибок

После того, как мы посмотрели на TRY-CATCH и SET XACT_ABORT ON, давайте соединим их вместе в примере, который мы можем использовать во всех наших хранимых процедурах. Для начала я покажу пример, в котором ошибка генерируется в простой форме, а в следующем разделе я рассмотрю решения получше.

Для примера я буду использовать эту простую таблицу.

CREATE TABLE sometable(a int NOT NULL,
                       b int NOT NULL,
                       CONSTRAINT pk_sometable PRIMARY KEY(a, b))

Вот хранимая процедура, которая демонстрирует, как вы должны работать с ошибками и транзакциями.

CREATE PROCEDURE insert_data @a int, @b int AS 
   SET XACT_ABORT, NOCOUNT ON
   BEGIN TRY
      BEGIN TRANSACTION
      INSERT sometable(a, b) VALUES (@a, @b)
      INSERT sometable(a, b) VALUES (@b, @a)
      COMMIT TRANSACTION
   END TRY
   BEGIN CATCH
      IF @@trancount > 0 ROLLBACK TRANSACTION
      DECLARE @msg nvarchar(2048) = error_message()  
      RAISERROR (@msg, 16, 1)
      RETURN 55555
   END CATCH

Первая строка в процедуре включает XACT_ABORT и NOCOUNT в одном выражении, как я показывал выше. Эта строка – единственная перед BEGIN TRY. Все остальное в процедуре должно располагаться после BEGIN TRY: объявление переменных, создание временных таблиц, табличных переменных, всё. Даже если у вас есть другие SET-команды в процедуре (хотя причины для этого встречаются редко), они должны идти после BEGIN TRY.

Причина, по которой я предпочитаю указывать SET XACT_ABORT и NOCOUNT перед BEGIN TRY, заключается в том, что я рассматриваю это как одну строку шума: она всегда должна быть там, но я не хочу, чтобы это мешало взгляду. Конечно же, это дело вкуса, и если вы предпочитаете ставить SET-команды после BEGIN TRY, ничего страшного. Важно то, что вам не следует ставить что-либо другое перед BEGIN TRY.

Часть между BEGIN TRY и END TRY является основной составляющей процедуры. Поскольку я хотел использовать транзакцию, определенную пользователем, я ввел довольно надуманное бизнес-правило, в котором говорится, что если вы вставляете пару, то обратная пара также должна быть вставлена. Два выражения INSERT находятся внутри BEGIN и COMMIT TRANSACTION. Во многих случаях у вас будет много строк кода между BEGIN TRY и BEGIN TRANSACTION. Иногда у вас также будет код между COMMIT TRANSACTION и END TRY, хотя обычно это только финальный SELECT, возвращающий данные или присваивающий значения выходным параметрам. Если ваша процедура не выполняет каких-либо изменений или имеет только одно выражение INSERT/UPDATE/DELETE/MERGE, то обычно вам вообще не нужно явно указывать транзакцию.

В то время как блок TRY будет выглядеть по-разному от процедуры к процедуре, блок CATCH должен быть более или менее результатом копирования и вставки. То есть вы делаете что-то короткое и простое и затем используете повсюду, не особо задумываясь. Обработчик CATCH, приведенный выше, выполняет три действия:

  1. Откатывает любые открытые транзакции.
  2. Повторно вызывает ошибку.
  3. Убеждается, что возвращаемое процедурой значение отлично от нуля.

Эти три действия должны всегда быть там. Мы можете возразить, что строка

IF @@trancount > 0 ROLLBACK TRANSACTION

не нужна, если нет явной транзакции в процедуре, но это абсолютно неверно. Возможно, вы вызываете хранимую процедуру, которая открывает транзакцию, но которая не может ее откатить из-за ограничений TRY-CATCH. Возможно, вы или кто-то другой добавите явную транзакцию через два года. Вспомните ли вы тогда о том, что нужно добавить строку с откатом? Не рассчитывайте на это. Я также слышу читателей, которые возражают, что если тот, кто вызывает процедуру, открыл транзакцию, мы не должны ее откатывать… Нет, мы должны, и если вы хотите знать почему, вам нужно прочитать вторую и третью части. Откат транзакции в обработчике CATCH – это категорический императив, у которого нет исключений.

Код повторной генерации ошибки включает такую строку:

DECLARE @msg nvarchar(2048) = error_message()

Встроенная функция error_message() возвращает текст возникшей ошибки. В следующей строке ошибка повторно вызывается с помощью выражения RAISERROR. Это не самый простой способ вызова ошибки, но он работает. Другие способы мы рассмотрим в следующей главе.

Замечание: синтаксис для присвоения начального значения переменной в DECLARE был внедрен в SQL Server 2008. Если у вас SQL Server 2005, вам нужно разбить строку на DECLARE и выражение SELECT.

Финальное выражение RETURN – это страховка. RAISERROR никогда не прерывает выполнение, поэтому выполнение следующего выражения будет продолжено. Пока все процедуры используют TRY-CATCH, а также весь клиентский код обрабатывает исключения, нет повода для беспокойства. Но ваша процедура может быть вызвана из старого кода, написанного до SQL Server 2005 и до внедрения TRY-CATCH. В те времена лучшее, что мы могли делать, это смотреть на возвращаемые значения. То, что вы возвращаете с помощью RETURN, не имеет особого значения, если это не нулевое значение (ноль обычно обозначает успешное завершение работы).

Последнее выражение в процедуре – это END CATCH. Никогда не следует помещать какой-либо код после END CATCH. Кто-нибудь, читающий процедуру, может не увидеть этот кусок кода.

После прочтения теории давайте попробуем тестовый пример:

EXEC insert_data 9, NULL

Результат выполнения:

Msg 50000, Level 16, State 1, Procedure insert_data, Line 12
Cannot insert the value NULL into column 'b', table 'tempdb.dbo.sometable'; column does not allow nulls. INSERT fails.

Давайте добавим внешнюю процедуру для того, чтобы увидеть, что происходит при повторном вызове ошибки:

CREATE PROCEDURE outer_sp @a int, @b int AS
   SET XACT_ABORT, NOCOUNT ON
   BEGIN TRY
      EXEC insert_data @a, @b
   END TRY
   BEGIN CATCH
      IF @@trancount > 0 ROLLBACK TRANSACTION
      DECLARE @msg nvarchar(2048) = error_message()
      RAISERROR (@msg, 16, 1)
      RETURN 55555
   END CATCH
go
EXEC outer_sp 8, 8

Результат работы:

Msg 50000, Level 16, State 1, Procedure outer_sp, Line 9
Violation of PRIMARY KEY constraint 'pk_sometable'. Cannot insert duplicate key in object 'dbo.sometable'. The duplicate key value is (8, 8).

Мы получили корректное сообщение об ошибке, но если вы посмотрите на заголовки этого сообщения и на предыдущее поближе, то можете заметить проблему:

Msg 50000, Level 16, State 1, Procedure insert_data, Line 12
Msg 50000, Level 16, State 1, Procedure outer_sp, Line 9

Сообщение об ошибке выводит информацию о расположении конечного выражения RAISERROR. В первом случае некорректен только номер строки. Во втором случае некорректно также имя процедуры. Для простых процедур, таких как наш тестовый пример, это не является большой проблемой. Но если у вас есть несколько уровней вложенных сложных процедур, то наличие сообщения об ошибке с отсутствием указания на место её возникновения сделает поиск и устранение ошибки намного более сложным делом. По этой причине желательно генерировать ошибку таким образом, чтобы можно было определить нахождение ошибочного фрагмента кода быстро, и это то, что мы рассмотрим в следующей главе.

4. Три способа генерации ошибки

4.1 Использование error_handler_sp

Мы рассмотрели функцию error_message(), которая возвращает текст сообщения об ошибке. Сообщение об ошибке состоит из нескольких компонентов, и существует своя функция error_xxx() для каждого из них. Мы можем использовать их для повторной генерации полного сообщения, которое содержит оригинальную информацию, хотя и в другом формате. Если делать это в каждом обработчике CATCH, это будет большой недостаток — дублирование кода. Вам не обязательно находиться в блоке CATCH для вызова error_message() и других подобных функций, и они вернут ту же самую информацию, если будут вызваны из хранимой процедуры, которую выполнит блок CATCH.

Позвольте представить вам error_handler_sp:

CREATE PROCEDURE error_handler_sp AS
 
   DECLARE @errmsg   nvarchar(2048),
           @severity tinyint,
           @state    tinyint,
           @errno    int,
           @proc     sysname,
           @lineno   int
           
   SELECT @errmsg = error_message(), @severity = error_severity(),
          @state  = error_state(), @errno = error_number(),
          @proc   = error_procedure(), @lineno = error_line()
       
   IF @errmsg NOT LIKE '***%'
   BEGIN
      SELECT @errmsg = '*** ' + coalesce(quotename(@proc), '<dynamic SQL>') + 
                       ', Line ' + ltrim(str(@lineno)) + '. Errno ' + 
                       ltrim(str(@errno)) + ': ' + @errmsg
   END
   RAISERROR('%s', @severity, @state, @errmsg)

Первое из того, что делает error_handler_sp – это сохраняет значение всех error_xxx() функций в локальные переменные. Я вернусь к выражению IF через секунду. Вместо него давайте посмотрим на выражение SELECT внутри IF:

SELECT @errmsg = '*** ' + coalesce(quotename(@proc), '<dynamic SQL>') + 
                 ', Line ' + ltrim(str(@lineno)) + '. Errno ' + 
                 ltrim(str(@errno)) + ': ' + @errmsg

Цель этого SELECT заключается в форматировании сообщения об ошибке, которое передается в RAISERROR. Оно включает в себя всю информацию из оригинального сообщения об ошибке, которое мы не можем вставить напрямую в RAISERROR. Мы должны обработать имя процедуры, которое может быть NULL для ошибок в обычных скриптах или в динамическом SQL. Поэтому используется функция COALESCE. (Если вы не понимаете форму выражения RAISERROR, я рассказываю о нем более детально во второй части.)

Отформатированное сообщение об ошибке начинается с трех звездочек. Этим достигаются две цели: 1) Мы можем сразу видеть, что это сообщение вызвано из обработчика CATCH. 2) Это дает возможность для error_handler_sp отфильтровать ошибки, которые уже были сгенерированы один или более раз, с помощью условия NOT LIKE ‘***%’ для того, чтобы избежать изменения сообщения во второй раз.

Вот как обработчик CATCH должен выглядеть, когда вы используете error_handler_sp:

BEGIN CATCH
   IF @@trancount > 0 ROLLBACK TRANSACTION
   EXEC error_handler_sp
   RETURN 55555
END CATCH

Давайте попробуем несколько тестовых сценариев.

EXEC insert_data 8, NULL
EXEC outer_sp 8, 8

Результат выполнения:

Msg 50000, Level 16, State 2, Procedure error_handler_sp, Line 20
*** [insert_data], Line 5. Errno 515: Cannot insert the value NULL into column 'b', table 'tempdb.dbo.sometable'; column does not allow nulls. INSERT fails.
Msg 50000, Level 14, State 1, Procedure error_handler_sp, Line 20
*** [insert_data], Line 6. Errno 2627: Violation of PRIMARY KEY constraint 'pk_sometable'. Cannot insert duplicate key in object 'dbo.sometable'. The duplicate key value is (8, 8).

Заголовки сообщений говорят о том, что ошибка возникла в процедуре error_handler_sp, но текст сообщений об ошибках дает нам настоящее местонахождение ошибки – как название процедуры, так и номер строки.

Я покажу еще два метода вызова ошибок. Однако error_handler_sp является моей главной рекомендацией для читателей, которые читают эту часть. Это — простой вариант, который работает на всех версиях SQL Server начиная с 2005. Существует только один недостаток: в некоторых случаях SQL Server генерирует два сообщения об ошибках, но функции error_xxx() возвращают только одну из них, и поэтому одно из сообщений теряется. Это может быть неудобно при работе с административными командами наподобие BACKUPRESTORE, но проблема редко возникает в коде, предназначенном чисто для приложений.

4.2. Использование ;THROW

В SQL Server 2012 Microsoft представил выражение ;THROW для более легкой обработки ошибок. К сожалению, Microsoft сделал серьезную ошибку при проектировании этой команды и создал опасную ловушку.

С выражением ;THROW вам не нужно никаких хранимых процедур. Ваш обработчик CATCH становится таким же простым, как этот:

BEGIN CATCH
   IF @@trancount > 0 ROLLBACK TRANSACTION
   ;THROW
   RETURN 55555
END CATCH

Достоинство ;THROW в том, что сообщение об ошибке генерируется точно таким же, как и оригинальное сообщение. Если изначально было два сообщения об ошибках, оба сообщения воспроизводятся, что делает это выражение еще привлекательнее. Как и со всеми другими сообщениями об ошибках, ошибки, сгенерированные ;THROW, могут быть перехвачены внешним обработчиком CATCH и воспроизведены. Если обработчика CATCH нет, выполнение прерывается, поэтому оператор RETURN в данном случае оказывается не нужным. (Я все еще рекомендую оставлять его, на случай, если вы измените свое отношение к ;THROW позже).

Если у вас SQL Server 2012 или более поздняя версия, измените определение insert_data и outer_sp и попробуйте выполнить тесты еще раз. Результат в этот раз будет такой:

Msg 515, Level 16, State 2, Procedure insert_data, Line 5
Cannot insert the value NULL into column 'b', table 'tempdb.dbo.sometable'; column does not allow nulls. INSERT fails.
Msg 2627, Level 14, State 1, Procedure insert_data, Line 6
Violation of PRIMARY KEY constraint 'pk_sometable'. Cannot insert duplicate key in object 'dbo.sometable'. The duplicate key value is (8, 8).

Имя процедуры и номер строки верны и нет никакого другого имени процедуры, которое может нас запутать. Также сохранены оригинальные номера ошибок.

В этом месте вы можете сказать себе: действительно ли Microsoft назвал команду ;THROW? Разве это не просто THROW? На самом деле, если вы посмотрите в Books Online, там не будет точки с запятой. Но точка с запятой должны быть. Официально они отделяют предыдущее выражение, но это опционально, и далеко не все используют точку с запятой в выражениях T-SQL. Более важно, что если вы пропустите точку с запятой перед THROW, то не будет никакой синтаксической ошибки. Но это повлияет на поведение при выполнении выражения, и это поведение будет непостижимым для непосвященных. При наличии активной транзакции вы получите сообщение об ошибке, которое будет полностью отличаться от оригинального. И еще хуже, что при отсутствии активной транзакции ошибка будет тихо выведена без обработки. Такая вещь, как пропуск точки с запятой, не должно иметь таких абсурдных последствий. Для уменьшения риска такого поведения, всегда думайте о команде как о ;THROW (с точкой с запятой).

Нельзя отрицать того, что ;THROW имеет свои преимущества, но точка с запятой не единственная ловушка этой команды. Если вы хотите использовать ее, я призываю вас прочитать по крайней мере вторую часть этой серии, где я раскрываю больше деталей о команде ;THROW. До этого момента, используйте error_handler_sp.

4.3. Использование SqlEventLog

Третий способ обработки ошибок – это использование SqlEventLog, который я описываю очень детально в третьей части. Здесь я лишь сделаю короткий обзор.

SqlEventLog предоставляет хранимую процедуру slog.catchhandler_sp, которая работает так же, как и error_handler_sp: она использует функции error_xxx() для сбора информации и выводит сообщение об ошибке, сохраняя всю информацию о ней. Вдобавок к этому, она логирует ошибку в таблицу splog.sqleventlog. В зависимости от типа приложения, которое у вас есть, эта таблица может быть очень ценным объектом.

Для использования SqlEventLog, ваш обработчик CATCH должен быть таким:

BEGIN CATCH
   IF @@trancount > 0 ROLLBACK TRANSACTION
   EXEC slog.catchhandler_sp @@procid
   RETURN 55555
END CATCH

@@procid возвращает идентификатор объекта текущей хранимой процедуры. Это то, что SqlEventLog использует для логирования информации в таблицу. Используя те же тестовые сценарии, получим результат их работы с использованием catchhandler_sp:

Msg 50000, Level 16, State 2, Procedure catchhandler_sp, Line 125
{515} Procedure insert_data, Line 5
Cannot insert the value NULL into column 'b', table 'tempdb.dbo.sometable'; column does not allow nulls. INSERT fails.
Msg 50000, Level 14, State 1, Procedure catchhandler_sp, Line 125
{2627} Procedure insert_data, Line 6
Violation of PRIMARY KEY constraint 'pk_sometable'. Cannot insert duplicate key in object 'dbo.sometable'. The duplicate key value is (8, 8).

Как вы видите, сообщение об ошибке отформатировано немного не так, как это делает error_handler_sp, но основная идея такая же. Вот образец того, что было записано в таблицу slog.sqleventlog:

logid logdate errno severity logproc linenum msgtext
1 2015-01-25 22:40:24.393 515 16 insert_data 5 Cannot insert …
2 2015-01-25 22:40:24.395 2627 14 insert_data 6 Violation of …

Если вы хотите попробовать SqlEventLog, вы можете загрузить файл sqleventlog.zip. Инструкция по установке находится в третьей части, раздел Установка SqlEventLog.

5. Финальные замечания

Вы изучили основной образец для обработки ошибок и транзакций в хранимых процедурах. Он не идеален, но он должен работать в 90-95% вашего кода. Есть несколько ограничений, на которые стоит обратить внимание:

  1. Как мы видели, ошибки компиляции не могут быть перехвачены в той же процедуре, в которой они возникли, а только во внешней процедуре.
  2. Пример не работает с пользовательскими функциями, так как ни TRY-CATCH, ни RAISERROR нельзя в них использовать.
  3. Когда хранимая процедура на Linked Server вызывает ошибку, эта ошибка может миновать обработчик в хранимой процедуре на локальном сервере и отправиться напрямую клиенту.
  4. Когда процедура вызвана как INSERT-EXEC, вы получите неприятную ошибку, потому что ROLLBACK TRANSACTION не допускается в данном случае.
  5. Как упомянуто выше, если вы используете error_handler_sp или SqlEventLog, мы потеряете одно сообщение, когда SQL Server выдаст два сообщения для одной ошибки. При использовании ;THROW такой проблемы нет.

Я рассказываю об этих ситуациях более подробно в других статьях этой серии.

Перед тем как закончить, я хочу кратко коснуться триггеров и клиентского кода.

Триггеры

Пример для обработки ошибок в триггерах не сильно отличается от того, что используется в хранимых процедурах, за исключением одной маленькой детали: вы не должны использовать выражение RETURN (потому что RETURN не допускается использовать в триггерах).

С триггерами важно понимать, что они являются частью команды, которая запустила триггер, и в триггере вы находитесь внутри транзакции, даже если не используете BEGIN TRANSACTION.
Иногда я вижу на форумах людей, которые спрашивают, могут ли они написать триггер, который не откатывает в случае падения запустившую его команду. Ответ таков: нет способа сделать это надежно, поэтому не стоит даже пытаться. Если в этом есть необходимость, по возможности не следует использовать триггер вообще, а найти другое решение. Во второй и третьей частях я рассматриваю обработку ошибок в триггерах более подробно.

Клиентский код

У вас должна быть обработка ошибок в коде клиента, если он имеет доступ к базе. То есть вы должны всегда предполагать, что при любом вызове что-то может пойти не так. Как именно внедрить обработку ошибок, зависит от конкретной среды.

Здесь я только обращу внимание на важную вещь: реакцией на ошибку, возвращенную SQL Server, должно быть завершение запроса во избежание открытых бесхозных транзакций:

IF @@trancount > 0 ROLLBACK TRANSACTION

Это также применимо к знаменитому сообщению Timeout expired (которое является не сообщением от SQL Server, а от API).

6. Конец первой части

Это конец первой из трех частей серии. Если вы хотели изучить вопрос обработки ошибок быстро, вы можете закончить чтение здесь. Если вы настроены идти дальше, вам следует прочитать вторую часть, где наше путешествие по запутанным джунглям обработки ошибок и транзакций в SQL Server начинается по-настоящему.

… и не забывайте добавлять эту строку в начало ваших хранимых процедур:

SET XACT_ABORT, NOCOUNT ON

Обработка ошибок

Последнее обновление: 14.08.2017

Для обработки ошибок в T-SQL применяется конструкция TRY…CATCH. Она имеет следующий формальный синтаксис:

BEGIN TRY
	инструкции
END TRY
BEGIN CATCH
	инструкции
END CATCH

Между выражениями BEGIN TRY и END TRY помещаются инструкции, которые потенциально могут вызвать ошибку, например,
какой-нибудь запрос. И если в этом блоке TRY возникнет ошибка, то управление передается в блок CATCH, где можно обработать ошибку.

В блоке CATCH для обаботки ошибки мы можем использовать ряд функций:

  • ERROR_NUMBER(): возвращает номер ошибки

  • ERROR_MESSAGE(): возвращает сообщение об ошибке

  • ERROR_SEVERITY(): возвращает степень серьезности ошибки. Степень серьезности представляет числовое значение. И если оно равно 10 и меньше, то
    такая ошибка рассматривается как предупреждение и не обрабатывается конструкцией TRY…CATCH. Если же это значение равно 20 и выше, то
    такая ошибка приводит к закрытию подключения к базе данных, если она не обрабатывается конструкцией TRY…CATCH.

  • ERROR_STATE(): возвращает состояние ошибки

Например, добавим в таблицу данные, которые не соответствуют ограничениям столбцов:

CREATE TABLE Accounts (FirstName NVARCHAR NOT NULL, Age INT NOT NULL)

BEGIN TRY
	INSERT INTO Accounts VALUES(NULL, NULL)
	PRINT 'Данные успешно добавлены!'
END TRY
BEGIN CATCH
	PRINT 'Error ' + CONVERT(VARCHAR, ERROR_NUMBER()) + ':' + ERROR_MESSAGE()
END CATCH

В данном случае для столбцов таблицы вставляются недопустимые данные — значения NULL, поэтому обработка программы перейдет к блоку CATCH:

TRY CATCH в T-SQL и MS SQL Server

Пересказ статьи Jared Westover. Raising Exceptions and Error Handling with SQL Server THROW

Рассматривали ли вы возможность добавления обработки ошибок в код Transact-SQL (T-SQL)? Если вы спросите опытных разработчиков, большинство из них согласится с тем, что это хорошая идея. Возможно, вам достался по наследству далеко не идеальный код. Или ваш код можно было бы немного привести в порядок. Основной причиной для добавления обработки ошибок является управление возникновением исключений. Было бы прекрасно, если бы ошибки не возникали, но такой мир не существует. Есть пара способов для вызова исключений в T-SQL. Более старый метод — это с использованием RAISERROR. Теперь RAISERROR все еще используется, но, начиная с SQL Server 2012 в городе появился новый игрок, которого зовут THROW.

Здесь мы рассмотрим использование THROW. Я укажу на некоторые преимущества и недостатки. В итоге вы сможете принять обоснованное решение о выборе способа реализации для ваших данных на SQL Server.

Использование оператора THROW в SQL Server

Обычно вы встречаете THROW внутри блока TRY…CATCH. Однако вы можете использовать THROW с параметрами отдельно. Давайте рассмотрим простой пример. Вы можете выполнять все приведенные здесь скрипты в SQL Server Management Studio (SSMS) без установки базы данных SQL Server.

THROW 50000, 'Houston, we have a problem.', 1;

Для работы вышеприведенного примера мы должны указать ID сообщения, текст и состояние. Что хорошо в случае THROW, не требуется, чтобы ID сообщения имелось в sys.messages. Кажется, я всегда использую 1 для состояния.

Как упоминалось выше, вы обычно используете THROW в сочетании с блоком TRY…CATCH, как показано в следующем примере.

BEGIN TRY -- Блок TRY
SELECT 1 / 0; -- Оператор SELECT
END TRY
BEGIN CATCH -- Блок CATCH
THROW;
END CATCH;

Замечательной особенностью THROW при использовании внутри TRY…CATCH является то, что вам нужно всего лишь напечатать THROW. Если бы существовала простая кнопка для вызова исключения, то это была бы она.

Преимущества THROW

Давайте потратим минутку и рассмотрим самые известные преимущества THROW, особенно в сравнении с RAISERROR. Сразу скажу, что простота THROW не имеет себе равных. Тем не менее, вот несколько других замечательных особенностей.

Сообщение об ошибке и номер строки

Посмотрите снова на скриншот выше. Вы получаете реальное сообщение, возвращаемое, если просто напечатать THROW. Вам не нужно беспокоиться об использовании функций обработки ошибок. Обратите внимание также на номер строки. Это номер строки, где произошло исключение, а не там, где мы вызывали THROW в блоке кода. Вообразите себе отладку кода в 1000+ строк. Получение точного номера критично при поиске возникающих ошибок.

ID сообщения

Если вы используете THROW вне блока TRY…CATCH, вам не нужно беспокоиться о наличии ID сообщения. Вы можете использовать любой старый номер. При использовании RAISERROR вам нужно было добавить сообщение в sys.messages, чтобы это работало.

Прерывание пакета

Когда я встречаю исключение, мне обычно хочется откатить транзакцию (ROLLBACK) и прервать выполнение пакета. Когда вы выполняете THROW, пакет останавливается, и SQL Server не выполняет никаких последующих операторов. Обратите внимание, что в примере ниже PRINT не выполняется. Вот синтаксис:

BEGIN TRY
SELECT 1 / 0;
END TRY
BEGIN CATCH
THROW;
PRINT 'Я очень надеюсь, что это сработает!';
END CATCH;

Рекомендации Microsoft

Наконец, начиная с SQL Server 2012, Microsoft рекомендовало использование THROW. Конечно, это, в первую очередь, зависит от того, зачем вы используете THROW. Я все еще использую RAISERROR в специальных случаях, что вы увидите в следующем разделе.

Недостатки THROW

Давайте потратим немного времени на рассмотрение некоторых недостатков THROW. Вот некоторые, которые обычно выделяют разработчики.

Информационные сообщения

Один из основных недостатков THROW — это невозможность вызова информационных сообщений. С RAISERROR вы можете использовать более низкие уровни серьезности. Например, уровень серьезности 10 возвращает информационное сообщение пользователю. Этот единственный недостаток оставляет RAISERROR в игре. Обратите внимание на черный текст на скриншоте ниже.

RAISERROR ('Now you know. And knowing is half the battle.',10,1)

Прерывание пакета

Я упоминал прерывание пакета в списке преимуществ, но это также и недостаток. Если вы хотите продолжить после возникновения исключения, для вас это недостаток. Такое поведение является еще одной причиной, почему я временами все еще использую RAISERROR. Говоря о THROW, я иногда сравниваю его с футбольной игрой, когда судья бросает флаг на поле. Когда флаг брошен, игра останавливается.

Отсутствие NOWAIT и WITHLOG

С RAISERROR вы можете выбрать запись сообщений в журнал ошибок SQL Server. Вы можете также вызвать исключение прежде, чем завершится весь пакет. THROW не предлагает ничего подобного. Я редко использую эти опции, поэтому это не тревожит меня.

Завершение оператора

Некоторые люди зацикливаются на необходимости завершить предыдущий оператор перед THROW. Для меня это не большая проблема, но позвольте привести пример. Если вы выполните нижеприведенный оператор, вы получите сообщение об ошибке при выполнении CATCH.

BEGIN TRY
BEGIN TRANSACTION;
SELECT 1 / 0;
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
IF (@@TRANCOUNT > 0)
ROLLBACK TRANSACTION
THROW;
END CATCH;

Чтобы обойти это, добавьте точку с запятой после ROLLBACK TRANSACTION или перед THROW. Я использовал ограничители операторов, по крайней мере, 10 лет, и советую другим делать то же самое.

Что использовать?

Фильм «Горец» был одним из лучших фильмов 80-х. Его слоганом было: «Должен остаться только один». В отличие от кино, вы не должны выбрать одно из. Вы можете использовать код ниже, если вы ищите ванильный метод для вызова исключений. Можете скопировать и вставить его в свой шаблон.

BEGIN TRY
BEGIN TRANSACTION;
-- Что-то делаем важное;
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
IF (@@TRANCOUNT > 0)
ROLLBACK TRANSACTION;
THROW;
END CATCH;

Однако из-за упомянутых выше недостатков не забывайте о RAISERROR. Ваш выбор зависит от цели. Для меня было бы много легче сказать, чтобы вы всегда делали то или это, но, как и для большинство вещей в SQL Server, ответ — «это зависит от…».

Summary: in this tutorial, you will learn how to use the SQL Server TRY CATCH construct to handle exceptions in stored procedures.

SQL Server TRY CATCH overview

The TRY CATCH construct allows you to gracefully handle exceptions in SQL Server. To use the TRY CATCH construct, you first place a group of Transact-SQL statements that could cause an exception in a BEGIN TRY...END TRY block as follows:

BEGIN TRY  
   -- statements that may cause exceptions
END TRY  
Code language: SQL (Structured Query Language) (sql)

Then you use a BEGIN CATCH...END CATCH block immediately after the TRY block:

BEGIN CATCH  
   -- statements that handle exception
END CATCH  
Code language: SQL (Structured Query Language) (sql)

The following illustrates a complete TRY CATCH construct:

BEGIN TRY  
   -- statements that may cause exceptions
END TRY 
BEGIN CATCH  
   -- statements that handle exception
END CATCH  
Code language: SQL (Structured Query Language) (sql)

If the statements between the TRY block complete without an error, the statements between the CATCH block will not execute. However, if any statement inside the TRY block causes an exception, the control transfers to the statements in the CATCH block.

The CATCH block functions

Inside the CATCH block, you can use the following functions to get the detailed information on the error that occurred:

  • ERROR_LINE() returns the line number on which the exception occurred.
  • ERROR_MESSAGE() returns the complete text of the generated error message.
  • ERROR_PROCEDURE() returns the name of the stored procedure or trigger where the error occurred.
  • ERROR_NUMBER() returns the number of the error that occurred.
  • ERROR_SEVERITY() returns the severity level of the error that occurred.
  • ERROR_STATE() returns the state number of the error that occurred.

Note that you only use these functions in the CATCH block. If you use them outside of the CATCH block, all of these functions will return NULL.

Nested TRY CATCH constructs

You can nest TRY CATCH construct inside another TRY CATCH construct. However, either a TRY block or a CATCH block can contain a nested TRY CATCH, for example:

BEGIN TRY
    --- statements that may cause exceptions
END TRY
BEGIN CATCH
    -- statements to handle exception
    BEGIN TRY
        --- nested TRY block
    END TRY
    BEGIN CATCH
        --- nested CATCH block
    END CATCH
END CATCH
Code language: SQL (Structured Query Language) (sql)

SQL Server TRY CATCH examples

First, create a stored procedure named usp_divide that divides two numbers:

CREATE PROC usp_divide(
    @a decimal,
    @b decimal,
    @c decimal output
) AS
BEGIN
    BEGIN TRY
        SET @c = @a / @b;
    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
END;
GO
Code language: SQL (Structured Query Language) (sql)

In this stored procedure, we placed the formula inside the TRY block and called the CATCH block functions ERROR_* inside the CATCH block.

Second, call the usp_divide stored procedure to divide 10 by 2:

DECLARE @r decimal;
EXEC usp_divide 10, 2, @r output;
PRINT @r;
Code language: SQL (Structured Query Language) (sql)

Here is the output

5
Code language: SQL (Structured Query Language) (sql)

Because no exception occurred in the TRY block, the stored procedure completed at the TRY block.

Third, attempt to divide 20 by zero by calling the usp_divide stored procedure:

DECLARE @r2 decimal;
EXEC usp_divide 10, 0, @r2 output;
PRINT @r2;
Code language: SQL (Structured Query Language) (sql)

The following picture shows the output:

SQL Server TRY CATCH Example

Because of division by zero error which was caused by the formula, the control was passed to the statement inside the CATCH block which returned the error’s detailed information.

SQL Serer TRY CATCH with transactions

Inside a CATCH block, you can test the state of transactions by using the XACT_STATE() function.

  • If the XACT_STATE() function returns -1, it means that an uncommittable transaction is pending, you should issue a ROLLBACK TRANSACTION statement.
  • In case the XACT_STATE() function returns 1, it means that a committable transaction is pending. You can issue a COMMIT TRANSACTION statement in this case.
  • If the XACT_STATE() function return 0, it means no transaction is pending, therefore, you don’t need to take any action.

It is a good practice to test your transaction state before issuing a COMMIT TRANSACTION or ROLLBACK TRANSACTION statement in a CATCH block to ensure consistency.

Using TRY CATCH with transactions example

First, set up two new tables sales.persons and sales.deals for demonstration:

CREATE TABLE sales.persons
(
    person_id  INT
    PRIMARY KEY IDENTITY, 
    first_name NVARCHAR(100) NOT NULL, 
    last_name  NVARCHAR(100) NOT NULL
);

CREATE TABLE sales.deals
(
    deal_id   INT
    PRIMARY KEY IDENTITY, 
    person_id INT NOT NULL, 
    deal_note NVARCHAR(100), 
    FOREIGN KEY(person_id) REFERENCES sales.persons(
    person_id)
);

insert into 
    sales.persons(first_name, last_name)
values
    ('John','Doe'),
    ('Jane','Doe');

insert into 
    sales.deals(person_id, deal_note)
values
    (1,'Deal for John Doe');
Code language: SQL (Structured Query Language) (sql)

Next, create a new stored procedure named usp_report_error that will be used in a CATCH block to report the detailed information of an error:

CREATE PROC usp_report_error
AS
    SELECT   
        ERROR_NUMBER() AS ErrorNumber  
        ,ERROR_SEVERITY() AS ErrorSeverity  
        ,ERROR_STATE() AS ErrorState  
        ,ERROR_LINE () AS ErrorLine  
        ,ERROR_PROCEDURE() AS ErrorProcedure  
        ,ERROR_MESSAGE() AS ErrorMessage;  
GO
Code language: SQL (Structured Query Language) (sql)

Then, develop a new stored procedure that deletes a row from the sales.persons table:

CREATE PROC usp_delete_person(
    @person_id INT
) AS
BEGIN
    BEGIN TRY
        BEGIN TRANSACTION;
        -- delete the person
        DELETE FROM sales.persons 
        WHERE person_id = @person_id;
        -- if DELETE succeeds, commit the transaction
        COMMIT TRANSACTION;  
    END TRY
    BEGIN CATCH
        -- report exception
        EXEC usp_report_error;
        
        -- Test if the transaction is uncommittable.  
        IF (XACT_STATE()) = -1  
        BEGIN  
            PRINT  N'The transaction is in an uncommittable state.' +  
                    'Rolling back transaction.'  
            ROLLBACK TRANSACTION;  
        END;  
        
        -- Test if the transaction is committable.  
        IF (XACT_STATE()) = 1  
        BEGIN  
            PRINT N'The transaction is committable.' +  
                'Committing transaction.'  
            COMMIT TRANSACTION;     
        END;  
    END CATCH
END;
GO
Code language: SQL (Structured Query Language) (sql)

In this stored procedure, we used the XACT_STATE() function to check the state of the transaction before performing COMMIT TRANSACTION or ROLLBACK TRANSACTION inside the CATCH block.

After that, call the usp_delete_person stored procedure to delete the person id 2:

EXEC usp_delete_person 2;
Code language: SQL (Structured Query Language) (sql)

There was no exception occurred.

Finally, call the stored procedure usp_delete_person to delete person id 1:

EXEC usp_delete_person 1;
Code language: SQL (Structured Query Language) (sql)

The following error occurred:

SQL Server TRY CATCH Transaction Example

In this tutorial, you have learned how to use the SQL Server TRY CATCH construct to handle exceptions in stored procedures.

In this tutorial, we will learn about exception handling in SQL Server 2019. We will learn about the exceptions, how and why the exceptions arise, and how to mitigate them.

  • What is Exception handling in SQL Server?
  • Types of exceptions in SQL Server
  • Try Catch in SQL Server
  • Exception handling in SQL Server stored procedure
  • Try catch in SQL Server stored procedure example
  • Throw a custom exception in SQL Server stored procedure
  • Throw a custom exception in SQL Server stored procedure example
  • Exception handling in SQL server function
  • SQL Server stored procedure error handling best practices

For all the examples, I have used sql server management studio and sql server 2019 express edition.

Some errors are generated by the application at runtime. These errors can be generated because of various reasons and the application is not able to handle these errors, causing the program to break and throw an exception.

Usually, these are generated because of unexpected human input such as passing an unexpected data type input e.g. passing an integer at the place of a string value, dividing a number by zero, etc.

But in real-life scenarios, the program is not supposed to be broken down, but execute flawlessly. Therefore, there is a need for a mechanism that can handle the situations that affect the program’s normal execution or cause it to break. This mechanism is called exception handling or error handling.

SQL Server also provides exception handling mechanism like any other programming language. There are different ways to handle the exceptions. We will discuss these in the upcoming sections with the help of examples.

  • Consider the following program which divides the two numbers:
USE [master]
GO
DECLARE
	@Number1 int,
	@Number2 int,
	@Result real

SET @Number1=10
SET @Number2=0

SET @Result=@Number1/@Number2
PRINT(@Result)
  • We know that dividing a number by 0 results in infinity. But let us see how the program executes it:
Exception Handling in SQL Server
Dividing a Number by Zero
  • Observe the above image. The program threw an exception with the message as ‘Divide by zero error encountered.’
  • You can also notice that no result was printed.
  • Imagine a real-life scenario, where you encountered such an exception and that exception caused your program to break. Therefore, to avoid such conditions, we use exception handling.

Read: Advanced Stored Procedure Examples in SQL Server

Types of exceptions in SQL Server

There are two types of exceptions in SQL Server. These are:

  1. System Defined Exceptions
  2. User-Defined Exceptions

System Defined Exceptions: These are the exceptions that are generated by the system during the program execution. The error messages are predefined in the system and the final user will see the predefined message on the console.

User-Defined Exceptions: These are the exceptions that are thrown explicitly by the programmer. The programmer can raise exceptions at any part of the SQL code according to the logic he has implemented in the program.

The programmer can also create custom messages that he finds will be interactive and meaningful to the user. He can also store these errors into log tables to maintain a log of generated exceptions.

Try Catch in SQL Server

In this section, we will learn how we use the Try-Catch block in SQL Server to handle exceptions. In SQL Server we use BEGIN TRY and BEGIN CATCH blocks to handle exceptions.

  • We put the SQL statements that may cause an exception in the BEGIN TRY block.
  • If any exception arises, the control is immediately transferred to the BEGIN CATCH block.
  • In the BEGIN CATCH block, we write the statements that should be executed if an exception is occurred in the BEGIN TRY block. That means the statements inside the BEGIN CATCH block are used for handling the flow of the program in case the exception is triggered.
  • If no exception arises i.e. all the SQL statements inside the BEGIN TRY block are executed, the control is not transferred to the BEGIN CATCH block.
try catch in sql server
BEGIN TRY-BEGIN CATCH Block Execution

Now let us see the general syntax for BEGIN TRY and BEGIN CATCH block.

BEGIN TRY
--SQL Statements where exception may encounter
END TRY

BEGIN CATCH
--SQL statements to handle exceptions
END CATCH
  • Now consider the above example i.e. the program to divide two numbers.
  • The above program can be re-written using exception handling as:
USE [master]
GO
DECLARE
	@Number1 int,
	@Number2 int,
	@Result real

SET @Number1=10
SET @Number2=0
BEGIN TRY
	SET @Result=@Number1/@Number2
	PRINT(@Result)
END TRY
BEGIN CATCH
	PRINT('You cannot divide a number by zero')
END CATCH
  • Now execute the above program. You will notice that this time also the result does not get printed. But, the statement inside the BEGIN CATCH block got executed.
  • Also, try to change the value from 0 to another integer value. This time you will see the expected result.

Exception handling in SQL Server stored procedure

Sometimes we have some statements inside a stored procedure that can generate run-time errors and causing the application to break. These can occur while inserting data into a table, updating data of a table, parsing input parameters etc.

In such situations, we can use exception handling in SQL Server stored procedure. In this section, we will understand how we can implement exception handling in stored procedures in SQL Server. We will understand this with an example in SQL Server 2019.

  • Let us consider the following example of a stored procedure that inserts a record into the Login table.
USE [master]
GO
CREATE PROCEDURE InsertRecord @Username nchar(20), @Password nchar(20)
AS
BEGIN
	INSERT INTO [dbo].[Login](Username, Password)
	VALUES(@Username, @Password)
END
  • The Username column is set as Primary Key in the Login table. This means that if we try to insert duplicate value in the Username column, we should get an error.
  • Try to insert duplicate value in this column by executing the procedure as:
USE [master]
GO
EXEC InsertRecord 'Brock', 'Brock@123'
Exception handling in SQL Server stored procedure
Encountered Exception
  • As a result, we encountered an exception and the program broke.
  • Now let us alter this stored procedure. This time write the stored procedure with the exception handling mechanism.
USE [master]
GO
ALTER PROCEDURE InsertRecord @Username nchar(20), @Password nchar(20)
AS
BEGIN
	BEGIN TRY
		INSERT INTO [dbo].[Login](Username, Password)
		VALUES(@Username, @Password)
	END TRY

	BEGIN CATCH
		PRINT('The username is already taken. Try another one')
	END CATCH
END
  • Exceute the stored procedure again with a duplicate value in the Username column.
USE [master]
GO
EXEC InsertRecord 'Brock', 'Brock@123'
Exception handling in SQL Server stored procedure example
Exception Handled
  • This time you can see that we did not get any error and our custom message is displayed.

Thus, you might have learned how we can implement exception handling inside a stored procedure in SQL Server. You can try some more examples. We will also discuss some more examples in this article.

Try catch in SQL Server stored procedure example

In this section, we will see another example of a stored procedure with an exception handling mechanism implemented. We will see how we can use Try-Catch in SQL Server stored procedure.

  • Consider the following stored procedure to print the sum of two numbers:
USE [master]
GO
CREATE PROCEDURE PrintSum @Number1 int, @Number2 int
AS
BEGIN
	DECLARE
		@Result int
	SET @Result=@Number1+@Number2
	PRINT('Sum is:'+@Result)
END
  • Everything is looking fine. However, it is not so. Execute the procedure with the following query:
USE [master]
GO
EXEC PrintSum 5, 7
  • You will encounter an exception. This is because when we use the PRINT statement to print a string along with a numeric value, we need to change the numeric value into a string data type before concatenation.
  • Now to handle this exception, we will use BEGIN TRY-BEGIN CATCH block.
  • Put the error-causing statement inside the BEGIN TRY block.
  • Write statements for handling the exception inside the BEGIN CATCH block.
USE [master]
GO
ALTER PROCEDURE PrintSum @Number1 int, @Number2 int
AS
BEGIN
	DECLARE
		@Result int
	SET @Result=@Number1+@Number2
	BEGIN TRY
		PRINT('Sum is:'+@Result)
	END TRY
	BEGIN CATCH
		SELECT  
			 ERROR_NUMBER() AS [Error Number]  
			,ERROR_SEVERITY() AS [Error Severity]  
			,ERROR_STATE() AS [Error State]  
			,ERROR_PROCEDURE() AS [Error Procedure]  
			,ERROR_MESSAGE() AS [Error Message]
			,ERROR_LINE() AS [Error Line];  
	END CATCH
END
  • As the exception is encountered, the control is transferred to the BEGIN CATCH block.
  • You can print the information about the exception with the help of some built-in functions.
  • These functions store the information about the exception that is encountered.
  • ERROR_NUMBER() returns the error number which is assigned to that particular error in the table of errors.
  • ERROR_SEVERITY() returns the severity of the exception. It stored an integer value depicting the severity. A higher value means that the severity of the error is high.
  • ERROR_STATE() returns the state number of the error. It returns a null value we call it outside the CATCH block.
  • ERROR_PROCEDURE() returns the name of the stored procedure in which the error occurred.
  • ERROR_MESSAGE() returns the message to be displayed to the user.
  • ERROR_LINE() returns the line number of the procedure at which the error occurred.
  • Execute the procedure to see the output.
USE [master]
GO
EXEC [dbo].PrintSum 5,7
try catch in sql server example
SQL Server Exception Handling

Thus, you might have learned about TRY CATCH and how we can use some built-in functions to generate a customized output for an exception.

Throw custom exception in SQL Server stored procedure

In SQL Server, you can also throw custom exceptions. Throwing a custom exception means you can cause an exception to encounter an error at any part of your code according to your logic.

We use the THROW statement or the RAISERROR function for throwing a custom exception. Both functions do almost the same work. However, there are some differences between the two.

  • The RAISERROR was introduced in SQL Server 7.0 and the THROW statement was introduced in SQL Server 2012. Microsoft suggests using the THROW statement as it is easier to use and has more functionalities than the RAISERROR function.
  • The RAISERROR function keeps the record of only the last exception thrown. However, it is not the case with the THROW statement. The THROW statement keeps a record of all the recent exceptions.
  • You can also use the THROW function without using any parameters specified. But, you cannot do this with the RAISERROR function.
  • The THROW statement threw an error. It takes three parameters:
    • Error number, which should be always greater than or equal to 50000 and less than the INT range.
    • Error message, which you want to show to the user. It should be enough informative to the user.
    • Error state, the current state of the error. It can have values between 0 to 255 as it supports the TINYINT data type.
  • You can also use the THROW() function without any parameter to throw an exception and transfer the control to the CATCH block, but there must be a CATCH block to handle the exception.

Throw custom exception in SQL Server stored procedure example

Now we will use the THROW statement in an example to see how we can generate custom exceptions in our stored procedure according to our logic.

  • We will create a stored procedure to check whether the product is available in the Products table or not.
USE [master]
GO
CREATE PROCEDURE FindProduct @ProductID int
AS
BEGIN
	IF EXISTS(SELECT * FROM Products where [Product ID]=@ProductID)
		PRINT('Product is Available in the Inventory')
	ELSE
		THROW 50001,'Error! No prodcuct with this ID',1
	PRINT('Thank You')
END
  • If you execute the procedure with a valid Product ID, it will display the message as ‘Product is Available in the Inventory’ and then ‘Thank You’ in the next line.
USE [master]
GO
EXEC FindProduct 1333
throw custom exception in sql server
Valid Product ID
  • But, when you execute the procedure with any invalid Product ID, it will throw an error with a custom error number, a custom message, and a custom state that you defined in the THROW statement.
USE [master]
GO
EXEC FindProduct 1333
throw custom exception sql server
Custom Exception Thrown when Invalid Product ID Provided
  • Note that the statements after the THROW will not execute.

Hence, you might have learned how we can use the throw statement in a stored procedure in SQL Server to throw a custom exception.

Read: SQL Server Replace Function + Example

Exception handling in SQL server function

In this section, we will learn how we can use exception handling or error handling with a function. SQL Server does not support writing exception handling blocks inside a function.

But there is an alternative. You can execute the function which may cause an error, inside a TRY CATCH block and handle the exception. Let us understand this concept with an example.

  • Let us take the example of a number divided by zero.
  • We will create a function to divide two numbers.
USE [master]
GO
CREATE FUNCTION [dbo].ThrowExcept(@Number1 int, @Number2 int)
RETURNS real
AS
BEGIN
	DECLARE
		@Result real
	SET @Result=@Number1/@Number2
	RETURN @Result
END
  • When we want to execute the function, we will execute it inside a TRY-CATCH block.
USE [master]
GO
DECLARE
	@result real
BEGIN TRY
	EXEC @result= [dbo].ThrowExcept 10, 2
	PRINT('Result is:'+STR(@result))
END TRY
BEGIN CATCH
	PRINT('You cannot divide a number by zero.
	Please change the second number')
END CATCH
  • This time, we will give normal input and we will get the expected result.
Exception handling in SQL Server fucntions
Exception Handling in SQL Server
  • Now execute the function again and this time give 0 as the second number.
  • The SQL Server will try to divide the number by zero and will throw an exception and transfer the control to the CATCH block.
  • The CATCH block will have the code to handle the exception.
USE [master]
GO
DECLARE
	@result real
BEGIN TRY
	EXEC @result= [dbo].ThrowExcept 10, 0
	PRINT('Result is:'+STR(@result))
END TRY
BEGIN CATCH
	PRINT('You cannot divide a number by zero.
	Please change the second number')
END CATCH
Exception handling in user defined function SQL Server
Result of Exception Handling in sql server

Thus, we cannot use the TRY CATCH block inside a function, but we can execute the function inside a TRY CATCH block to handle the exception encountered during the function’s execution.

SQL Server stored procedure error handling best practices

In this section, you will learn how you should use exception handling in the best way. We will share some tips regarding the use of exception handling in SQL server 2019.

  • Always use the TRY CATCH block where you think there is a logical group of statements. In these situations, there are higher chances of errors.
  • Try to hide the technical details from the user. The user may not understand the technical details sometimes. These can also confuse the user.
  • Print user-friendly messages. The message that you print should be clear and easy to understand for the user.
  • Use generic code for error handling. Do not use too much complex code for exception handling, such as nesting at multiple levels.
  • Create log tables to store the information about the errors encountered at the time of execution. This sometimes helps in a situation when you want to refer to any previous error.
  • Use stored procedures for storing logs.
  • Create some particularly stored procedures to handle the errors. Call the suitable stored procedure in the CATCH block to handle a particular type of exception.

You may like the following sql server tutorials:

  • SQL Server Convert Datetime to String
  • How to execute function in SQL with parameters
  • SQL Server Agent won’t start

Thus, that was all about this article. Hope you have learned enough about error handling or exception handling in SQL Server. You can use it in your own ways for practice.

  • What is Exception handling in SQL Server?
  • Types of exceptions in SQL Server
  • Try Catch in SQL Server
  • Exception handling in SQL Server stored procedure
  • Try catch in SQL Server stored procedure example
  • Throw custom exception in SQL Server stored procedure
  • Throw custom exception in SQL Server stored procedure example
  • Exception handling in SQL server function
  • SQL Server stored procedure error handling best practices

Bijay

I am Bijay having more than 15 years of experience in the Software Industry. During this time, I have worked on MariaDB and used it in a lot of projects. Most of our readers are from the United States, Canada, United Kingdom, Australia, New Zealand, etc.

Want to learn MariaDB? Check out all the articles and tutorials that I wrote on MariaDB. Also, I am a Microsoft MVP.

Понравилась статья? Поделить с друзьями:
  • Sql server код ошибки 3417
  • Sql server native client ошибка выделения памяти
  • Sql server management studio ошибка при установке
  • Sql server management studio журнал ошибок
  • Sql server agent ошибка 1067