Incorrect Syntax Near in SQL Server (Error 102)

🚨Part of the SQL Server Errors series, the exact messages and what actually causes them.

Msg 102  ·  Level 15  ·  State 1
Incorrect syntax near ‘0’.
If the same statement runs on one database and fails on another, check compatibility_level before you check your syntax. Msg 102 is decided by the parser, and which grammar the parser uses depends on the database you are sitting in.

Most pages about error 102 tell you to look for a typo. That advice is fine for the five minutes where it is true, and useless for the case that actually costs a day: the statement is correct, you have run it before, and it works on the server next to this one. That is the version of 102 worth writing about, so it is most of what follows.

Everything below was measured on SQL Server 2025 (17.0.4075.5) rather than recalled, because the compatibility rules move between builds and a remembered answer here is worth very little.


Read the Quoted Token Literally, Then Look Left

The token in quotes is where the parser gave up, not where you went wrong. The mistake is nearly always immediately before it, and often a character or two earlier than you would guess. Incorrect syntax near ')' usually means something is missing inside the brackets. Incorrect syntax near 'FROM' means the problem is in the select list behind it.

Two neighbours of 102 say more than 102 does, and it is worth knowing you have been handed one of them instead:

MsgTextWhat it narrows down
102Incorrect syntax near '%.*ls'.The parser hit a token it cannot place. Widest of the three, and the one this page is about.
156Incorrect syntax near the keyword '%.*ls'.The token is a reserved word. Either you used one as a name, or a clause is out of order.
105Unclosed quotation mark after the character string '%.*ls'.A quote is open. Everything after it was read as string, which is why the reported position looks nowhere near the real one.

All three are severity 15 and none of them are written to the error log, so there is nothing to go back and find later. Whatever you have is what you get.


The Compatibility Level Trap

Here is the part that catches people. The parser’s grammar is selected by the compatibility level of the database you are connected to, not by the version of the instance. A SQL Server 2025 instance will happily refuse 2022 syntax if the database you happen to be sitting in is still set to 130.

That produces the symptom that sends people looking for a typo they do not have: the same text, on the same server, in the same session, parses in one database and throws 102 in another.

I ran exactly that, with SET PARSEONLY ON so nothing executed and no data was touched. Same instance, same batch, two databases, the only difference being compatibility_level:

StatementCompat 130Compat 170
SELECT TRIM(LEADING '0' FROM '007');Msg 102: Incorrect syntax near ‘0’.Parsed OK
SELECT object_id, COUNT(*) OVER w FROM sys.objects WINDOW w AS (PARTITION BY type);Msg 102: Incorrect syntax near ‘w’.Parsed OK

Note where the parser pointed. On the TRIM line it blamed '0', which is the correct string literal in a perfectly valid statement. The actual problem is the LEADING keyword before it, which the 130 grammar does not know, so it read TRIM(, expected an expression, and complained about the first thing it did not expect. The token in the message was innocent. That is the general case, not a quirk of this example.

Check the level before you check anything else:

SELECT DB_NAME() AS current_database,
       (SELECT compatibility_level FROM sys.databases WHERE name = DB_NAME()) AS compat;

-- everything on the instance, and where it differs from the instance default
SELECT d.name, d.compatibility_level
FROM sys.databases d
ORDER BY d.compatibility_level, d.name;

A database restored from an older instance keeps the level it arrived with. Nothing raises it for you, so a 2016-era database can sit on a 2025 instance for years, working perfectly, until someone writes a statement in syntax it has never supported.


What Compatibility Level Does Not Gate

It would be neat if every newer feature failed the same way at a lower level. It does not, and assuming it does will send you down the wrong path, so here is the honest boundary.

Only things baked into the grammar produce 102. Functions are resolved later, after parsing, so a function the level supposedly gates does not fail at parse time at all. On the same compat 130 database that rejected TRIM(LEADING ...), all of these parsed and then ran without complaint:

Measured, compat 130, SQL Server 2025 GENERATE_SERIES(1,3) ran and returned rows. DATE_BUCKET(DAY, 1, ...) ran and returned a value. 1 IS DISTINCT FROM 2 parsed cleanly. All three are documented as newer than that level, and none of them produced Msg 102 here.

So the rule to carry away is narrower than “new syntax needs a new compat level”. It is: if the feature changes the shape of the statement, the level can reject it with 102; if the feature is a function call, it will not. A missing function gives you Msg 208 or “is not a recognized built-in function name” instead, which are different problems with different fixes.

Do not take the list above as a promise for your build either. Test it on the instance in front of you, the same way, with SET PARSEONLY ON. It costs one query and nothing runs.


102 or 156? They Are Not Interchangeable

A lot of writing about this treats 102 and 156 as the same error. They are not, and the difference tells you which mistake you made. Two of the classics people file under 102 actually return 156, measured on the same instance:

StatementWhat actually came back
SELECT 1 AS table;Msg 156: Incorrect syntax near the keyword ‘table’.
SELECT name, FROM sys.objects;Msg 156: Incorrect syntax near the keyword ‘FROM’.
SELECT 1 WHERE 1 IN ();Msg 102: Incorrect syntax near ‘)’.

The trailing comma is the useful one. It reads like a comma problem and reports as a keyword problem, because the parser only knows something is wrong when it reaches FROM and finds no expression in front of it. If you are handed a 156 naming a keyword you did not think you had misused, the mistake is in what comes before that keyword.

Reserved words used as names are worth fixing rather than quoting your way around, but if you must, bracket them: SELECT 1 AS [table] parses fine.


When the Editor and the Server Disagree

SSMS parses your query locally before the server ever sees it, using the grammar that shipped with that copy of SSMS. So there are two separate ways to be confused here, and they point in opposite directions:

  • Red underline, but it runs. Your SSMS is older than the syntax. The server accepted it, the editor simply did not recognise it. Update SSMS, or ignore the squiggle.
  • No underline, but Msg 102 on execute. Your SSMS is newer than the database’s compatibility level. The editor knows the syntax, the database does not. This is the trap above, and it is the more common of the two now that SSMS updates independently of the engine.

The same split explains why a query pasted from a colleague fails for you and not them. Before assuming the text changed in transit, compare DB_NAME() and the compatibility level on both sides.


You Cannot Catch 102 With TRY/CATCH

Parsing happens before any of the batch runs, so a batch containing a syntax error never starts, including its CATCH block. Wrapping the statement in TRY changes nothing, because there is no execution to protect.

Move the text into sp_executesql and parsing moves to execution time, at which point an outer CATCH does see it. That is not a workaround to leave in production code, but it is genuinely useful for testing, and it is exactly how the measurements on this page were collected:

-- Does this statement parse in THIS database, at THIS compatibility level?
-- PARSEONLY means nothing executes, so it is safe to run anywhere.
BEGIN TRY
    EXEC sp_executesql N'SET PARSEONLY ON; SELECT TRIM(LEADING ''0'' FROM ''007'');';
    SELECT DB_NAME() AS db, 'parsed OK' AS result;
END TRY
BEGIN CATCH
    SELECT DB_NAME() AS db,
           'Msg ' + CAST(ERROR_NUMBER() AS varchar(10)) + ': ' + ERROR_MESSAGE() AS result;
END CATCH

Run that in the database that fails and again in one you know is current. Two rows, one difference, and you have your answer without changing anything.


Before You Raise the Compatibility Level

Raising the level is usually the right end state, and it is not a syntax fix. It changes the query optimizer’s behaviour as well as the grammar, so plans can change on statements that had nothing to do with your error. Treat it as a change with a test pass behind it, not as a one-line answer to a parse failure.

If you need the statement working today and the level cannot move yet, rewrite it in syntax the current level supports. The named WINDOW clause becomes a repeated OVER (...); TRIM(LEADING ...) becomes the older LTRIM or a PATINDEX expression. Less tidy, and it runs today on the database you actually have.


Common Questions

The query works on my dev server and fails in production. Same script, same version.
Compare compatibility_level on the two databases, not the two instances. The engine version being identical is not enough, and a database restored from an older server keeps its old level indefinitely. This is the single most common cause of a 102 that “should not happen”.
How do I know whether it is a real typo or a compatibility problem?
Run the identical statement under SET PARSEONLY ON in a database at the current level, such as master. If it parses there and not in yours, the text is fine and the level is the difference. If it fails in both, it is a typo, and the quoted token tells you roughly where to look.
Why does the error point at something that is obviously correct?
Because the quoted token is where the parser stopped, not where the statement went wrong. In the measured TRIM(LEADING '0' FROM '007') case it blamed '0', a perfectly valid literal, when the unrecognised keyword was LEADING just before it. Read leftwards from the token, not at it.
Is Msg 102 the same as Msg 156?
No. 156 fires when the token is a reserved word, which is a narrower and more useful signal: either you used a keyword as a name, or a clause is in the wrong order. Both are severity 15. A trailing comma before FROM returns 156, not 102, which surprises people who have filed every syntax error under 102.
Will raising the compatibility level break anything?
It can. The level drives optimizer behaviour as well as the grammar, so execution plans on unrelated statements can change. Raise it deliberately, with Query Store on so you can see what moved and revert a regressed plan, rather than as a quick fix for one parse error.
Does error 102 appear in the SQL Server error log?
No. sys.messages shows is_event_logged = 0 for 102, 156 and 105 alike. There is no server-side record to go back for, so whatever the application captured at the time is all the evidence there will be.

Related Scripts

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *