В Microsoft SQL Server ошибку перехватывают с помощью конструкции BEGIN TRY ... END TRY BEGIN CATCH ... END CATCH: если инструкция в блоке TRY падает с ошибкой уровня выше 10, выполнение переходит в блок CATCH. Там функции ERROR_NUMBER(), ERROR_MESSAGE() и остальные из семейства ERROR_* сообщают номер, текст и строку ошибки, а ROLLBACK TRANSACTION и THROW откатывают изменения и передают ошибку дальше.
Конструкция похожа на try/catch в C# или JavaScript, но блока finally в T-SQL (Transact-SQL) нет. Все примеры запускались в SQL Server 2025 (версия 17.0), оператор THROW есть начиная с SQL Server 2012.
- Синтаксис BEGIN TRY и BEGIN CATCH
- Функции ERROR_NUMBER, ERROR_MESSAGE и другие
- Какие ошибки TRY CATCH не перехватывает
- TRY CATCH и транзакции: XACT_STATE и SET XACT_ABORT
- THROW и RAISERROR: чем отличаются
- Точка с запятой перед THROW
- Шаблон хранимой процедуры с обработкой ошибок
- Повтор транзакции при взаимоблокировке
- TRY CATCH в функциях и триггерах
- TRY CATCH в PostgreSQL и MySQL
- Частые вопросы
- Чем TRY CATCH лучше проверки @@ERROR?
- Как вернуть текст ошибки в приложение?
Синтаксис BEGIN TRY и BEGIN CATCH
BEGIN TRY
-- инструкции, которые могут упасть
END TRY
BEGIN CATCH
-- обработка ошибки
END CATCH;
Блок CATCH идёт сразу после END TRY: инструкция между ними даёт синтаксическую ошибку. Конструкция живёт в одном пакете и не может начаться в одной ветке IF ... ELSE, а закончиться в другой. Если в блоке TRY ошибок не было, CATCH пропускается и выполняется код после END CATCH.
Простейший пример с делением на ноль:
BEGIN TRY
SELECT 1 / 0 AS Result;
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;
ErrorNumber ErrorSeverity ErrorState ErrorProcedure ErrorLine ErrorMessage
8134 16 1 NULL 2 Divide by zero error encountered.
Перед этой таблицей SQL Server вернёт пустой набор Result: SELECT упал на вычислении. До клиента ошибка не дошла, её перехватил CATCH.
Функции ERROR_NUMBER, ERROR_MESSAGE и другие
| Функция | Что возвращает |
|---|---|
ERROR_NUMBER() | номер ошибки |
ERROR_SEVERITY() | уровень серьёзности |
ERROR_STATE() | номер состояния |
ERROR_PROCEDURE() | имя процедуры или триггера, где произошла ошибка; в примере выше NULL |
ERROR_LINE() | номер строки; в процедуре счёт идёт от начала пакета с её CREATE PROCEDURE |
ERROR_MESSAGE() | полный текст сообщения с подставленными именами и значениями |
Вне блока CATCH все шесть функций возвращают NULL. Внутри они работают в любом месте блока и в вызванной из него процедуре, так что сбор сведений об ошибке можно вынести в отдельную процедуру.
Блоки TRY CATCH можно вкладывать друг в друга, и каждый CATCH видит свою ошибку. Мы поставили в CATCH от деления на ноль вложенный TRY с CAST('abc' AS int): вложенный CATCH получил номер 245 (ошибка преобразования), а внешний после него снова вернул 8134.

Какие ошибки TRY CATCH не перехватывает
Блок CATCH не сработает в четырёх случаях:
- предупреждения и сообщения с уровнем 10 и ниже:
RAISERROR(N'...', 10, 1)в блоке TRY просто напечатает текст, и выполнение пойдёт дальше; - ошибки уровня 20 и выше, после которых сервер закрывает соединение;
- прерывание запроса клиентом, обрыв соединения и
KILLот администратора; - ошибки компиляции и разрешения имён объектов на том же уровне, что и сам TRY.
Поэтому опечатка в имени таблицы проходит мимо CATCH:
BEGIN TRY
SELECT * FROM dbo.NoSuchTable;
END TRY
BEGIN CATCH
SELECT ERROR_NUMBER() AS ErrorNumber;
END CATCH;
Msg 208, Level 16, State 1, Line 2
Invalid object name 'dbo.NoSuchTable'.
Если тот же SELECT лежит в хранимой процедуре, ошибка возникает уровнем ниже и CATCH её ловит:
CREATE PROCEDURE dbo.usp_ReadMissing
AS
SELECT * FROM dbo.NoSuchTable;
GO
BEGIN TRY
EXEC dbo.usp_ReadMissing;
END TRY
BEGIN CATCH
SELECT ERROR_NUMBER() AS ErrorNumber, ERROR_LINE() AS ErrorLine;
END CATCH;
CATCH вернул номер 208 и строку 3, то есть строку SELECT внутри процедуры. Так же работает динамический SQL через sp_executesql.
TRY CATCH и транзакции: XACT_STATE и SET XACT_ABORT
Без TRY CATCH и с настройками по умолчанию (XACT_ABORT выключен) перевод между счетами может создать деньги из ничего. Таблица счетов запрещает отрицательный баланс:
CREATE TABLE Accounts (
Id int NOT NULL CONSTRAINT PK_Accounts PRIMARY KEY,
Balance decimal(10, 2) NOT NULL CONSTRAINT CK_Accounts_Balance CHECK (Balance >= 0)
);
INSERT INTO Accounts (Id, Balance) VALUES (1, 100), (2, 50);
GO
BEGIN TRANSACTION;
UPDATE Accounts SET Balance = Balance + 200 WHERE Id = 2;
UPDATE Accounts SET Balance = Balance - 200 WHERE Id = 1;
COMMIT TRANSACTION;
Msg 547, Level 16, State 0, Line 3
The UPDATE statement conflicted with the CHECK constraint "CK_Accounts_Balance". The conflict occurred in database "Bank", table "dbo.Accounts", column 'Balance'.
The statement has been terminated.
Учебная база здесь называется Bank, у вас в сообщении будет имя своей базы. После этого SELECT показывает на первом счёте 100, на втором 250. Списание отменилось, зачисление осталось, и COMMIT TRANSACTION его зафиксировал. По умолчанию SET XACT_ABORT выключен, и при части ошибок SQL Server откатывает одну упавшую инструкцию, а транзакция продолжается.

Правильный вариант с TRY CATCH:
SET XACT_ABORT ON;
BEGIN TRY
BEGIN TRANSACTION;
UPDATE Accounts SET Balance = Balance + 200 WHERE Id = 2;
UPDATE Accounts SET Balance = Balance - 200 WHERE Id = 1;
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
SELECT XACT_STATE() AS XactState,
@@TRANCOUNT AS TranCount,
ERROR_NUMBER() AS ErrorNumber;
IF @@TRANCOUNT > 0
ROLLBACK TRANSACTION;
END CATCH;
XactState TranCount ErrorNumber
-1 1 547
Балансы остались 100 и 50. XACT_STATE() в блоке CATCH показывает, что можно сделать с транзакцией:
| Значение | Что значит |
|---|---|
| 1 | транзакция открыта, её можно зафиксировать или откатить |
| 0 | открытой транзакции нет, ROLLBACK и COMMIT дадут ошибку |
| -1 | транзакция незафиксируемая: разрешены только чтение и полный ROLLBACK |
С выключенным XACT_ABORT тот же код вернул XactState = 1: COMMIT в блоке CATCH прошёл бы и зафиксировал половину перевода. Ставьте SET XACT_ABORT ON в каждую процедуру с явной транзакцией. Без TRY ошибка выполнения при этой настройке откатывает транзакцию целиком. Внутри TRY транзакция становится незафиксируемой (XACT_STATE() = -1), и CATCH обязан сделать ROLLBACK. На синтаксические ошибки настройка не влияет, RAISERROR её не учитывает. В триггерах она включена по умолчанию.
THROW и RAISERROR: чем отличаются
THROW без параметров внутри CATCH пробрасывает пойманную ошибку как есть. RAISERROR создаёт новую, и при повторном выбросе исходный номер теряется. Вставим дубликат ключа и перебросим ошибку двумя способами:
BEGIN TRY
INSERT INTO Accounts (Id, Balance) VALUES (1, 10);
END TRY
BEGIN CATCH
DECLARE @msg nvarchar(2048) = ERROR_MESSAGE();
RAISERROR(@msg, 16, 1); -- второй вариант: THROW;
END CATCH;
-- RAISERROR
Msg 50000, Level 16, State 1, Line 6
Violation of PRIMARY KEY constraint 'PK_Accounts'. Cannot insert duplicate key in object 'dbo.Accounts'. The duplicate key value is (1).
-- THROW
Msg 2627, Level 14, State 1, Line 2
Violation of PRIMARY KEY constraint 'PK_Accounts'. Cannot insert duplicate key in object 'dbo.Accounts'. The duplicate key value is (1).
Приложение, которое ловит нарушение уникальности по номеру 2627, после RAISERROR его не узнает.
RAISERROR | THROW | |
|---|---|---|
| Номер своей ошибки | из sys.messages или 50000 для текста | любой от 50 000 до 2 147 483 647 |
Форматирование %s, %d | есть | нет, знак % в тексте пишут как %% |
| Уровень серьёзности | задаётся | всегда 16, при повторном выбросе исходный |
SET XACT_ABORT ON | не учитывает | откатывает транзакцию |
| Повторный выброс | только новой ошибкой | THROW; без параметров |
В новом коде используйте THROW. Своя ошибка выглядит так: THROW 50001, N'Недостаточно средств на счёте 1', 1;.
Точка с запятой перед THROW
Инструкция перед THROW должна заканчиваться точкой с запятой. Если её нет, возможны два исхода, и оба видны на запуске:
BEGIN CATCH
SELECT N'Ошибка поймана'
THROW;
END CATCH;
Здесь THROW стал псевдонимом столбца: запрос вернул колонку с именем THROW и значением «Ошибка поймана», а ошибка молча пропала. Во втором случае точки с запятой нет после ROLLBACK TRANSACTION:
IF @@TRANCOUNT > 0
ROLLBACK TRANSACTION
THROW;
SQL Server принял THROW за имя транзакции и ответил ошибкой 6401 «Cannot roll back THROW. No transaction or savepoint of that name was found.» Исходная ошибка потерялась, а при выключенном XACT_ABORT транзакция осталась открытой: @@TRANCOUNT после этого вернул 1.

Шаблон хранимой процедуры с обработкой ошибок
Соберём всё в хранимую процедуру перевода денег. Сведения об ошибке пишем в таблицу журнала:
CREATE TABLE ErrorLog (
Id int IDENTITY PRIMARY KEY,
ErrorNumber int,
ErrorSeverity int,
ErrorState int,
ErrorProcedure nvarchar(128),
ErrorLine int,
ErrorMessage nvarchar(4000),
LoggedAt datetime2 NOT NULL DEFAULT SYSDATETIME()
);
GO
CREATE PROCEDURE dbo.usp_Transfer
@FromId int,
@ToId int,
@Amount decimal(10, 2)
AS
BEGIN
SET NOCOUNT ON;
SET XACT_ABORT ON;
BEGIN TRY
IF @Amount <= 0
THROW 50001, N'Сумма перевода должна быть больше нуля', 1;
BEGIN TRANSACTION;
UPDATE Accounts SET Balance = Balance - @Amount WHERE Id = @FromId;
UPDATE Accounts SET Balance = Balance + @Amount WHERE Id = @ToId;
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0
ROLLBACK TRANSACTION;
INSERT INTO ErrorLog
(ErrorNumber, ErrorSeverity, ErrorState, ErrorProcedure, ErrorLine, ErrorMessage)
VALUES
(ERROR_NUMBER(), ERROR_SEVERITY(), ERROR_STATE(),
ERROR_PROCEDURE(), ERROR_LINE(), ERROR_MESSAGE());
THROW;
END CATCH;
END;
В CATCH сначала идёт ROLLBACK: в незафиксируемой транзакции INSERT в журнал запрещён, а в обычной запись откатилась бы вместе с переводом. Потом журнал. Последним идёт THROW, иначе ошибка не дойдёт до приложения: всё, что поймал CATCH, вызывающий код сам не видит.
Вызовем EXEC dbo.usp_Transfer 1, 2, 30 на счетах 100 и 50, затем ту же процедуру с суммами 500 и 0. Первый перевод прошёл, второй вернул ошибку 547 от CHECK, третий нашу 50001. На счетах 70 и 80, в журнале две строки:
ErrorNumber ErrorProcedure ErrorLine ErrorMessage
547 dbo.usp_Transfer 15 The UPDATE statement conflicted with the CHECK constraint "CK_Accounts_Balance". ...
50001 dbo.usp_Transfer 12 Сумма перевода должна быть больше нуля
ERROR_LINE() считает строки от начала пакета с CREATE PROCEDURE: строка 15 соответствует списанию, строка 12 проверке суммы.

Повтор транзакции при взаимоблокировке
Если две транзакции ждут друг друга, SQL Server выбирает одну жертвой, откатывает её транзакцию и возвращает ошибку 1205. CATCH её ловит, и транзакцию можно повторить после короткой паузы. Повтор пишется циклом WHILE:
DECLARE @Attempt int = 1;
WHILE @Attempt <= 3
BEGIN
BEGIN TRY
BEGIN TRANSACTION;
UPDATE Accounts SET Balance = Balance - 10 WHERE Id = 1;
UPDATE Accounts SET Balance = Balance + 10 WHERE Id = 2;
COMMIT TRANSACTION;
BREAK;
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0
ROLLBACK TRANSACTION;
IF ERROR_NUMBER() <> 1205 OR @Attempt = 3
THROW;
SET @Attempt += 1;
WAITFOR DELAY '00:00:02';
END CATCH;
END;
Паузу перед повтором лучше делать случайной длины, например 1-3 секунды: так две сессии реже повторяют попытку одновременно и снова блокируют друг друга.
Любую другую ошибку цикл сразу пробрасывает: повторять нарушение CHECK бессмысленно.
TRY CATCH в функциях и триггерах
В пользовательской функции TRY CATCH запрещён: функцию с BEGIN TRY внутри сервер не создаст и ответит ошибкой 443 «Invalid use of a side-effecting operator ‘BEGIN TRY’ within a function.» Проверки в функции пишут условиями с RETURN, а ошибку ловят там, откуда функцию вызвали.
Ошибка в триггере без собственного TRY CATCH попадает в CATCH вызывающего кода, если инструкция, которая запустила триггер, стоит в блоке TRY. Мы проверили это триггером AFTER INSERT с делением на ноль: CATCH получил ошибку 8134, ERROR_PROCEDURE() вернула имя триггера, а строка в таблицу не вставилась.
TRY CATCH в PostgreSQL и MySQL
В PostgreSQL и MySQL конструкции BEGIN TRY нет. Таблица accounts в примерах та же: балансы 100 и 50 и CHECK (balance >= 0). В PostgreSQL роль TRY CATCH играет блок EXCEPTION в PL/pgSQL:
DO $$
BEGIN
UPDATE accounts SET balance = balance + 200 WHERE id = 2;
UPDATE accounts SET balance = balance - 200 WHERE id = 1;
EXCEPTION
WHEN check_violation THEN
RAISE NOTICE 'Перевод отменён: % (SQLSTATE %)', SQLERRM, SQLSTATE;
END $$;
При перехвате PostgreSQL откатывает изменения внутри блока, и балансы остались 100 и 50 без явного ROLLBACK. Блок с EXCEPTION дороже обычного, без нужды его не ставят.
В MySQL 8.4 в начале процедуры объявляют обработчик ошибок:
DELIMITER //
CREATE PROCEDURE transfer(IN from_id INT, IN to_id INT, IN amount DECIMAL(10, 2))
BEGIN
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
RESIGNAL;
END;
START TRANSACTION;
UPDATE accounts SET balance = balance + amount WHERE id = to_id;
UPDATE accounts SET balance = balance - amount WHERE id = from_id;
COMMIT;
END //
DELIMITER ;
CALL transfer(1, 2, 200) вернул «Check constraint ‘accounts_chk_1’ is violated.», балансы не изменились. RESIGNAL без параметров передаёт ошибку дальше без изменений, как THROW; в SQL Server.

Частые вопросы
Чем TRY CATCH лучше проверки @@ERROR?
@@ERROR хранит номер ошибки только до следующей инструкции, и проверять его приходится после каждой строки. TRY CATCH ловит ошибку в любом месте блока, а ERROR_* дают ещё текст и строку.
Как вернуть текст ошибки в приложение?
Закончить CATCH инструкцией THROW;: клиент получит исходные номер, уровень и текст. Если вместо этого вывести текст через PRINT или SELECT, для приложения запрос завершится без ошибки.








