The Problem
You're writing error handling in T-SQL:
BEGIN TRY
INSERT INTO Orders (OrderID, CustomerID) VALUES (1001, NULL);
END TRY
BEGIN CATCH
THROW 51000, 'Invalid customer ID', 1; -- ← Missing semicolon before THROW
END CATCH;
You expect an exception to be thrown. Instead, SQL Server silently ignores the THROW and continues. No error, no exception, no alert. Your error handling is broken, and you won't know until someone finds a data corruption bug at 3 AM.
Why This Happens
THROW is Not Like Other Statements
Most T-SQL statements are optional-semicolon-friendly:
-- These all work fine without semicolons at the end:
INSERT INTO Orders VALUES (1001, 5)
UPDATE Orders SET Status = 'Shipped'
DELETE FROM Orders WHERE OrderID = 1001
SELECT * FROM Orders
But THROW is different. It's a control flow statement (like RETURN, BREAK in some contexts). SQL Server's parser has a rule: THROW requires a statement terminator (semicolon) before it.
The SQL Standard
In SQL Server 2012 (when THROW was introduced), Microsoft made statement terminators mandatory for certain control flow statements. THROW is one of them.
This is stricter than other statements because THROW can appear in complex exception-handling contexts where the parser needs to know exactly where the previous statement ends.
The Invisible Failure
What Actually Happens
Without the semicolon, SQL Server interprets the code differently:
-- What you wrote:
BEGIN CATCH
THROW 51000, 'Invalid customer ID', 1;
END CATCH;
-- What SQL Server might parse as:
BEGIN CATCH
-- THROW 51000... is part of the previous statement somehow
-- (Parser confusion)
END CATCH;
The THROW doesn't execute. No exception is raised. The CATCH block "succeeds" silently. Execution continues as if nothing went wrong.
It Looks Like it Works
The code compiles. No syntax error. You test it casually and assume it works because you don't hit the error condition. Then in production, when an error should be thrown, it isn't. Data gets corrupted. Reconciliation fails. Customers notice.
Real-World Example: The Silent Bug
A developer wrote a stored procedure to process orders:
CREATE PROCEDURE InsertOrder
@OrderID INT,
@CustomerID INT
AS
BEGIN
BEGIN TRY
IF @CustomerID IS NULL
THROW 50001, 'Customer ID cannot be null', 1; -- ← No semicolon!
INSERT INTO Orders (OrderID, CustomerID)
VALUES (@OrderID, @CustomerID);
SELECT 'Success' AS Result;
END TRY
BEGIN CATCH
SELECT ERROR_MESSAGE() AS ErrorMessage;
END CATCH;
END;
GO
-- Testing
EXEC InsertOrder @OrderID = 1001, @CustomerID = NULL;
-- Expected: Error 50001
-- Actual: 'Success' (because THROW didn't execute!)
The procedure appears to succeed. NULL is inserted into CustomerID. Later, reports fail because they assume CustomerID is never NULL. Hours of debugging, and someone finally notices the missing semicolon.
When THROW Must Have a Semicolon
Inside Exception Handlers
BEGIN CATCH
;THROW -- ← Semicolon required (or before the previous statement)
END CATCH;
After Other Statements
UPDATE Orders SET Status = 'Shipped'
;THROW 50001, 'Error', 1; -- ← Semicolon before THROW required
In Procedures and Batches
CREATE PROCEDURE ValidateOrder @ID INT AS
BEGIN
IF NOT EXISTS (SELECT 1 FROM Orders WHERE OrderID = @ID)
;THROW 50002, 'Order not found', 1; -- ← Semicolon required
SELECT 'Valid' AS Result;
END;
When THROW Does NOT Need a Semicolon
THROW stands alone at the start of a batch:
THROW 50001, 'Error message', 1;
-- This can work without a preceding semicolon because THROW is the first statement
But even then, it's safer to include the semicolon for consistency.
The Fix: Always Precede THROW with a Semicolon
Rule: Semicolon Before THROW
-- BAD
BEGIN CATCH
THROW 51000, 'Error', 1;
END CATCH;
-- GOOD
BEGIN CATCH
;THROW 51000, 'Error', 1;
END CATCH;
Or: Terminate the Previous Statement
-- BAD
UPDATE Orders SET Status = 'Shipped'
THROW 51000, 'Error', 1;
-- GOOD
UPDATE Orders SET Status = 'Shipped';
THROW 51000, 'Error', 1;
Best Practice: Always Terminate Statements
Modern T-SQL best practice is to terminate every statement with a semicolon, not just THROW:
BEGIN TRY
IF @Value IS NULL
;THROW 50001, 'Value cannot be null', 1;
INSERT INTO LogTable (Message) VALUES ('Processing...'); -- ← Semicolon
UPDATE Counters SET Count = Count + 1; -- ← Semicolon
SELECT 'Success' AS Result; -- ← Semicolon (optional at end, but good style)
END TRY
BEGIN CATCH
;THROW; -- ← Re-throw with leading semicolon
END CATCH;
Compare: THROW vs. RAISERROR
The legacy RAISERROR statement doesn't have this requirement:
-- RAISERROR works without preceding semicolon
BEGIN CATCH
RAISERROR ('Error message', 16, 1); -- ← No semicolon needed
END CATCH;
-- THROW requires preceding semicolon
BEGIN CATCH
;THROW 51000, 'Error message', 1; -- ← Semicolon required
END CATCH;
This is why many developers miss the THROW semicolon rule — they're used to RAISERROR's looser syntax.
Subtle Variations: When It Breaks
Scenario 1: Nested Control Flow
-- Broken
BEGIN TRANSACTION
IF @Valid = 0
THROW 50001, 'Invalid', 1; -- ← Missing semicolon, inside IF
COMMIT;
END TRY
-- Fixed
BEGIN TRANSACTION
IF @Valid = 0
;THROW 50001, 'Invalid', 1;
COMMIT;
END TRY;
Scenario 2: Multiple Statements in CATCH
-- Broken
BEGIN CATCH
LOG_ERROR; -- User-defined procedure
THROW 51000, 'Error', 1; -- ← THROW not preceded by semicolon, follows EXEC
END CATCH;
-- Fixed
BEGIN CATCH
LOG_ERROR;
;THROW 51000, 'Error', 1; -- ← Leading semicolon
END CATCH;
Scenario 3: THROW After SELECT
-- Broken
BEGIN CATCH
SELECT ERROR_NUMBER() AS ErrorNum;
THROW 51000, 'Re-throwing', 1; -- ← Previous SELECT not terminated
END CATCH;
-- Fixed
BEGIN CATCH
SELECT ERROR_NUMBER() AS ErrorNum;
;THROW 51000, 'Re-throwing', 1; -- ← Semicolon before THROW
END CATCH;
Detection: How to Find This Bug
Static Analysis
Search your codebase for THROW without a preceding semicolon:
-- Find potential issues in your code
-- Search for pattern: [^;]\n\s*THROW
-- Or: Look for lines with THROW not immediately preceded by semicolon
Code Review Checklist
- Every THROW statement has a semicolon before it
- That semicolon terminates the previous statement
- If using leading semicolon style (;THROW), it's consistently applied
Testing
Always test exception paths:
-- Test that THROW actually throws
CREATE PROCEDURE TestThrow AS
BEGIN
BEGIN TRY
;THROW 50001, 'Test exception', 1;
END TRY
BEGIN CATCH
SELECT ERROR_NUMBER() AS Num; -- Should print 50001
END CATCH;
END;
EXEC TestThrow;
-- If it prints 50001, THROW worked
-- If it doesn't, the THROW wasn't executed (bug!)
Best Practices
1. Use Leading Semicolon Style
Place the semicolon before THROW, not after the previous statement:
BEGIN CATCH
;THROW 51000, ERROR_MESSAGE(), 1;
END CATCH;
This style makes it explicit that the semicolon belongs to THROW, not the previous statement.
2. Terminate All Statements
Use semicolons for every T-SQL statement, not just THROW:
SELECT * FROM Orders;
UPDATE Orders SET Status = 'Shipped';
INSERT INTO Audit VALUES ('Updated');
;THROW 51000, 'Error', 1;
3. Format Consistently
-- Use a standard pattern for all error handling
BEGIN TRY
-- ... your code ...
END TRY
BEGIN CATCH
;THROW; -- Re-throw the caught exception
END CATCH;
4. Document It
If your team isn't familiar with this rule, add a comment:
BEGIN CATCH
-- Note: Semicolon before THROW is required (T-SQL parsing rule)
;THROW 51000, 'Validation failed', 1;
END CATCH;
5. Use a Linter
If using SQL Server code analysis tools, enable rules for statement terminators:
- SQL Server Data Tools (SSDT) rule: ST009 (Use semicolon after statement)
- dbForge Studio: Enable statement terminator validation
- SonarQube for SQL: Checks for missing terminators
The Verdict
THROW requires a preceding semicolon. Forget it, and your exception handling silently fails. This rule is strict, unusual, and easy to miss — but critical to get right.
The solution is simple: always precede THROW with a semicolon, or adopt the best practice of terminating every statement with one.
This is one of those rules that seems pedantic until it costs you 4 hours of debugging at midnight. After that, you'll never forget it.