powershelldba.de

THROW and the Semicolon: SQL Server's Invisible Gotcha

THROW requires a preceding semicolon — a statement terminator. Forget it, and your code silently fails to throw. This small rule causes big bugs.

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

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:

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.