PL/SQL & T-SQL (Oracle and SQL Server) Module 3 – T-SQL errors and transactions
T-SQL TRY...CATCH, THROW and RAISERROR
Catch T-SQL errors with TRY...CATCH and the ERROR functions, raise your own with THROW or RAISERROR, and see which errors no CATCH block receives.
What you will learn
- Catch run-time errors with TRY...CATCH and read ERROR_NUMBER, ERROR_MESSAGE, ERROR_LINE and ERROR_PROCEDURE
- Explain why an error inside a transaction can still end in a partial commit
- Report application errors with THROW, and choose between THROW and RAISERROR
- Recognise the errors that a CATCH block at the same level never receives
- Handle cleanup and logging in a CATCH block, then raise the original error again with THROW
Before you start
What you’ll learn · Before you start
What you will learn
- Catch run-time errors with TRY...CATCH and read ERROR_NUMBER, ERROR_MESSAGE, ERROR_LINE and ERROR_PROCEDURE
- Explain why an error inside a transaction can still end in a partial commit
- Report application errors with THROW, and choose between THROW and RAISERROR
- Recognise the errors that a CATCH block at the same level never receives
- Handle cleanup and logging in a CATCH block, then raise the original error again with THROW
Before you start
On this page
A transfer of 5,000.00 inside BEGIN TRANSACTION fails on its debit with error 547, the credit after it runs anyway,
and the COMMIT at the end keeps that credit: 5,000.00 created from nothing. T-SQL reports many run-time errors
without stopping the batch, so code that must stop has to catch the error and decide itself what to undo.
You will catch errors with T-SQL TRY…CATCH and read what the ERROR_ functions report, reproduce the partial commit
that an uncaught error leaves behind, raise your own errors with THROW and RAISERROR, meet the errors that no CATCH
block at the same level receives, and write a CATCH block that cleans up, logs and raises the original error again.
A TRY block hands its first error to the CATCH block
T-SQL TRY…CATCH is a pair of blocks: BEGIN TRY … END TRY, followed at once by BEGIN CATCH … END CATCH. When a statement in the TRY block raises an
error, the rest of the block is skipped and control passes to the first statement of the CATCH block; when nothing
fails, the CATCH block is skipped. Microsoft’s page states what is caught: every run-time error with a
error severity: The level from 0 to 25 that SQL Server gives every message: 10 or lower is information, 11 to 16 errors the user can correct, 17 to 19 software errors, and 20 or more fatal errors that stop the task. above 10 that does not close the database connection. An error inside a
procedure called from the TRY block is caught too, unless the procedure has a TRY...CATCH of its own. A
TRY...CATCH cannot span two batches and cannot be used inside a user-defined function. Step through a transfer that
fails inside a procedure:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- The call starts. The TRY block opens a transaction and calls dbo.post_pair to move 5,000.00 from account 3 to account 1. The procedure has no TRY...CATCH of its own.
- The credit succeeds. The first UPDATE adds 5,000.00 to account 1, which now holds 7,500.00 inside the open transaction. Nothing is committed yet.
- The debit fails. The second UPDATE would take account 3 from 760.25 to below zero, so the CHECK constraint accounts_balance_ck refuses it with error 547, severity 16.
- Control jumps to CATCH. The error leaves the procedure and lands in the caller's CATCH block. The rest of the TRY block, its COMMIT and its PRINT, never runs.
- CATCH reads the details. Inside the CATCH block the ERROR_ functions describe the error. ERROR_NUMBER gives 547, ERROR_PROCEDURE dbo.post_pair and ERROR_LINE the line of the failed UPDATE.
- CATCH rolls back. @@TRANCOUNT is still 1, so the CATCH block rolls back and the credit disappears too: both balances are as they were. The error is not sent to the client unless the CATCH block sends it.
Inside the CATCH block, six functions describe the error, and the table after the script lists what each returns for
the failed debit. Outside the CATCH block, each returns NULL, which the script’s last SELECT shows:
-- Engine: SQL Server 2025 CU9 (17.0.5005.3); database lab with the practice schema. Client: sqlcmd.
-- An error inside a procedure, caught by the caller's TRY...CATCH, with the six ERROR_ functions. dbo.post_pair moves
-- money between two accounts; account 3 holds 760.25, so taking 5,000.00 from it breaks accounts_balance_ck.
CREATE OR ALTER PROCEDURE dbo.post_pair
@from int,
@to int,
@amount decimal(14,2)
AS
BEGIN
SET NOCOUNT ON;
UPDATE dbo.accounts SET balance = balance + @amount WHERE account_id = @to; -- the credit runs first
UPDATE dbo.accounts SET balance = balance - @amount WHERE account_id = @from; -- error 547 when @from is short
END;
GO
SET NOCOUNT ON;
BEGIN TRY
BEGIN TRANSACTION;
EXEC dbo.post_pair @from = 3, @to = 1, @amount = 5000.00;
COMMIT TRANSACTION; -- skipped: control left the TRY block at the error
PRINT N'not printed';
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION; -- undo the credit as well
SELECT ERROR_NUMBER() AS number, -- 547
ERROR_SEVERITY() AS severity, -- 16
ERROR_STATE() AS state,
ERROR_PROCEDURE() AS in_procedure, -- dbo.post_pair (with the schema since SQL Server 2017)
ERROR_LINE() AS at_line, -- the line of the second UPDATE inside the procedure
ERROR_MESSAGE() AS message; -- ... conflicted with the CHECK constraint "accounts_balance_ck" ...
END CATCH;
SELECT ERROR_NUMBER() AS outside_a_catch_block; -- NULL
SELECT account_id, balance FROM dbo.accounts WHERE account_id IN (1, 3) ORDER BY account_id; -- 2500.00, 760.25
DROP PROCEDURE dbo.post_pair;
GO """try_catch_basics.sql in the lab: sqlcmd runs it as sa in the database lab, on a freshly loaded practice schema."""
from mssql_session import Lab
with Lab() as lab:
lab.sqlcmd("basics/try_catch_basics.sql") Output
$ sqlcmd -S localhost -U sa -C -d lab -i basics/try_catch_basics.sql
number severity state in_procedure at_line message
------ -------- ----- ------------- ------- -----------------------------------------------------------------------------------------------------------------------------------------------------------------
547 16 0 dbo.post_pair 12 The UPDATE statement conflicted with the CHECK constraint "accounts_balance_ck". The conflict occurred in database "lab", table "dbo.accounts", column 'balance'.
outside_a_catch_block
---------------------
NULL
account_id balance
---------- -------
1 2500.00
3 760.25
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 try_catch_basics_session.py
| Function | What it returns for the failed debit |
|---|---|
ERROR_NUMBER() |
547, the CHECK constraint violation |
ERROR_SEVERITY() |
16, an error the user can correct |
ERROR_STATE() |
the state number the error was raised with |
ERROR_PROCEDURE() |
dbo.post_pair, where the error happened (the schema is part of the name since SQL Server 2017), or NULL outside a procedure or trigger |
ERROR_LINE() |
the line of the failed UPDATE inside the procedure |
ERROR_MESSAGE() |
the full text, with the constraint, table and column filled in |
One more rule changes how callers see errors: Microsoft notes that an error trapped by a CATCH block is not returned
to the calling application. Unless the CATCH block sends it on, the client sees a batch that succeeded.
Sending an error on, and raising errors of your own, is the work of two statements. THROW raises an error with your own number and message, and Microsoft recommends it for new code. RAISERROR is the older statement, which formats its message and still runs in much existing code.
Checkpoint
Check: TRY...CATCH and the ERROR_ functions
Partial commits, and raising errors with THROW and RAISERROR Pro: opens with a Pro, Premium or Ultimate pass
An error inside a transaction does not stop the transaction, and a procedure that finds a broken rule must raise an error of its own. This part reproduces the transfer that commits half of its work, then raises errors with THROW and RAISERROR and compares the two.
In this part:
- An error inside a transaction ends only its own statement
- THROW raises your own errors, and RAISERROR formats them
4 runnable examples (Python, SQL) · about 7 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Uncaught errors, and a CATCH block in the right order Pro: opens with a Pro, Premium or Ultimate pass
A CATCH block does not receive every error, and the order of its steps decides what the caller and the log see. This part lists the errors that escape it, then writes the block that copies the details, rolls back, logs and raises the original error again.
In this part:
- Some errors never reach the CATCH block at their own level
- Copy the details, roll back, log, then THROW again
1 diagram · 5 runnable examples (Python, SQL) · about 9 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Practise: follow the error to where it lands
Each question shows a statement that fails, or a CATCH block. Decide where control goes, what is kept and what the
caller sees, then read why each answer is right or wrong:
Practice that adapts to you
Practise this lesson
3 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up (fresher to 2–5 years): what happens when a statement inside a TRY block fails, and what does the caller see?
The rest of the TRY block is skipped and control passes to the first statement of the CATCH block; when nothing
fails, the CATCH block is skipped. The CATCH block receives every run-time error with a severity above 10 that does
not close the connection, including one raised inside a procedure that the TRY block called, unless that procedure
has a TRY...CATCH of its own. Inside the CATCH block, ERROR_NUMBER(), ERROR_SEVERITY(), ERROR_STATE(),
ERROR_PROCEDURE(), ERROR_LINE() and ERROR_MESSAGE() describe the error, and outside it they return NULL. A caught
error is not returned to the caller: unless the CATCH block sends it on, the client sees a batch that succeeded.
Two longer questions follow, one about a transfer that committed half of its work and one about an error log that stays empty; answer each before you open its first step:
-
Mid to senior
A transfer procedure runs a credit and a debit between BEGIN TRANSACTION and COMMIT, with no error handling. The finance team finds transfers where the credit happened and the debit did not, all on days when a balance check failed. Explain how that was possible, fix the procedure, and say how you would find every affected transfer.
-
Mid to senior
A procedure's CATCH block inserts into dbo.error_log, rolls back and then runs RAISERROR(ERROR_MESSAGE(), 16, 1). After a week, the log table is empty although failures happened, and the calling application no longer recognises deadlocks and constraint errors. Explain both problems and rewrite the CATCH block.
Flashcards · 3 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
Which errors does a TRY block hand to its CATCH block?
Every run-time error of severity above 10 that does not close the database connection, including one raised inside a procedure the TRY block called, unless that procedure has a TRY...CATCH of its own. A TRY...CATCH cannot span two batches and cannot be used inside a user-defined function.
-
What do the ERROR_ functions report, and where do they answer?
ERROR_NUMBER, ERROR_SEVERITY, ERROR_STATE, ERROR_PROCEDURE (schema included since SQL Server 2017, NULL outside a procedure or trigger), ERROR_LINE and ERROR_MESSAGE. They answer inside the CATCH block only: outside it each returns NULL.
-
A CATCH block logs the error and ends. What does the calling application see?
A batch that succeeded. Microsoft notes that an error trapped by a CATCH block is not returned to the calling application unless the block sends it on.
Key takeaways
- A
TRYblock passes its first run-time error of severity 11 or more to theCATCHblock, where theERROR_functions describe it, and a caught error is not returned to the client.
Terms in this lesson
- error severity
- The level from 0 to 25 that SQL Server gives every message: 10 or lower is information, 11 to 16 errors the user can correct, 17 to 19 software errors, and 20 or more fatal errors that stop the task.
References
- TRY...CATCH (Transact-SQL) (Microsoft)
- THROW (Transact-SQL) (Microsoft)
- RAISERROR (Transact-SQL) (Microsoft)
- FORMATMESSAGE (Transact-SQL) (Microsoft)
- ERROR_NUMBER (Transact-SQL) (Microsoft)
- ERROR_PROCEDURE (Transact-SQL) (Microsoft)
- Database engine error severities (Microsoft)
- SET XACT_ABORT (Transact-SQL) (Microsoft)
- ROLLBACK TRANSACTION (Transact-SQL) (Microsoft)
- sp_executesql (Transact-SQL) (Microsoft)
- Database engine events and errors (0 to 999) (Microsoft)
Report a problem with this lesson
Kept only in this browser. Your Learn progress