SELECT DB_NAME(), SCHEMA_NAME() before anything else. Four contexts produce this error and three of them are about where you are sitting, not whether the object exists.The table almost always exists. What has gone wrong is the name was resolved in a context you did not expect, and there are only four contexts that matter: the database you are connected to, the schema you default to, whether the object was ever really there, and whether the batch has even reached the point of resolving it yet.
Answer the Four Questions in Order
SELECT DB_NAME() AS connected_to,
SCHEMA_NAME() AS your_default_schema,
OBJECT_ID('Orders') AS unqualified_resolves_to,
OBJECT_ID('dbo.Orders') AS dbo_resolves_to;
A NULL in either OBJECT_ID column tells you the name does not resolve as you wrote it, from where you are sitting. That is a different statement from “the table does not exist”.
| What you see | What it means |
|---|---|
connected_to is not the database you expected | The connection string or the SSMS database dropdown, not the query |
unqualified_resolves_to is NULL but dbo_resolves_to is not | Your default schema is not dbo. Qualify the name |
| Both NULL | The object is genuinely absent from this database, or the name is misspelled |
| Both NULL, but it works for a colleague | You are on different databases, or different schemas, or both |
The Database Context Trap, and the Two-Second Fix
This is the most common cause in practice and the least interesting, which is why it is worth ruling out first. SSMS opens new query windows against your login's default database, which is very often master. The query is correct. You are simply running it somewhere else.
The dropdown at the top of the query window shows where you actually are. Change it, or be explicit and let the query say it:
USE YourDatabase;
GO
SELECT * FROM dbo.Orders;
-- or reference it across databases with a three-part name
SELECT * FROM YourDatabase.dbo.Orders;
Two related cases the four-question query will not catch. A four-part name (Server.Database.Schema.Object) resolves through a linked server, so 208 there can mean the linked server is fine and the object is missing on the far side, or that the remote login maps to a principal that cannot see it. And SSMS IntelliSense caches object names, so a table created in another window is underlined in red and reported as invalid by the editor while the server is perfectly happy. Press Ctrl+Shift+R to refresh the local cache. If the squiggle goes away without you changing anything, the server was never the problem.
Find Where It Actually Lives
SELECT s.name AS schema_name,
o.name AS object_name,
o.type_desc,
o.create_date
FROM sys.objects AS o
JOIN sys.schemas AS s ON s.schema_id = o.schema_id
WHERE o.name = N'Orders';
Run that in the database you believe the object is in. If it comes back empty, run it in the others before concluding anything. A table sitting in a staging or import schema is the single most common answer, and an unqualified reference will never find it.
The Default-Schema Trap
This is the one that produces “it works on my machine”. A user created without an explicit DEFAULT_SCHEMA gets dbo, but a user mapped through a Windows group, or created with a schema of their own name, does not.
SELECT dp.name AS principal_name,
dp.type_desc,
dp.default_schema_name
FROM sys.database_principals AS dp
WHERE dp.type IN ('S','U','G')
AND dp.name = N'DOMAIN\username';
Two people run the same unqualified query, resolve it against two different schemas, and get two different answers. Qualifying every object as schema.object removes this entire class of problem, and it is faster too, because an unqualified name forces a lookup against the caller’s schema before falling back to dbo.
Deferred Name Resolution, or Why the Procedure Created Fine
SQL Server lets you create a stored procedure that references a table which does not exist. The reference is resolved when the statement runs, not when the procedure is created.
CREATE OR ALTER PROCEDURE dbo.usp_Broken
AS
SELECT * FROM dbo.TableThatIsNotHere; -- creates without complaint
GO
So a clean deployment proves nothing about whether every object referenced inside it exists. It also means 208 can appear from a procedure that has been in production for months, the first time a rarely-hit branch executes.
The same applies to a temporary table created inside an IF branch. If the branch does not run, the table is not there, but the batch was still compiled.
Is It Actually a Permissions Problem?
This is the question everyone asks second, and the answer is more useful than a yes or no. Referencing an object you have no rights on does not give you 208. It gives you a permissions error that names the object plainly. Measured on SQL Server 2025, with a user granted nothing on the objects it touched:
| What you did | What SQL Server returns |
|---|---|
SELECT a table in this database you have no rights on | Msg 229 — The SELECT permission was denied on the object… |
EXEC a procedure you have no rights on | Msg 229 — The EXECUTE permission was denied on the object… |
Reference OtherDb.dbo.Thing with no user in OtherDb | Msg 916 — The server principal is not able to access the database… |
| Reference something that genuinely is not there | Msg 208 — Invalid object name |
So if you are holding a 208, permissions are not the direct cause. Chase the four contexts above instead.
The trap is one level up, in the tooling. Metadata visibility has been restricted since SQL Server 2005: you cannot see an object you have no permission on. In the same test the privileged user saw all four objects in sys.objects and the low-privilege user saw one. And this is the part that bites:
-- as a user with no rights on dbo.NoRights
SELECT OBJECT_ID('dbo.NoRights'); -- returns NULL, not an error
OBJECT_ID() returns NULL, which is exactly what it returns for an object that does not exist. Every deployment script, ORM and migration tool that guards with IF OBJECT_ID('dbo.Thing') IS NULL therefore concludes the object is missing and moves on to create it, and the CREATE then fails saying the object already exists. That contradictory pair, “it does not exist” followed by “it already exists”, is a permissions problem wearing a 208-shaped costume. Check with a permission-aware function instead:
SELECT HAS_PERMS_BY_NAME('dbo.NoRights', 'OBJECT', 'SELECT') AS can_select,
OBJECT_ID('dbo.NoRights') AS object_id_says;
-- can_select = 0 with object_id_says = NULL means "there, but not for you"
You Cannot Catch 208 With TRY/CATCH
Worth knowing before you write a handler for it. 208 is raised at compile time, when the batch is bound, not while it runs. The whole batch including the TRY block fails to compile, so the CATCH never executes:
BEGIN TRY
SELECT * FROM dbo.NoSuchTable; -- batch never compiles
END TRY
BEGIN CATCH
SELECT 'never reached' AS this; -- CATCH does not run
END CATCH
To handle it you have to push the reference into a separate batch so binding happens at execution time:
BEGIN TRY
EXEC sp_executesql N'SELECT * FROM dbo.NoSuchTable;';
END TRY
BEGIN CATCH
SELECT ERROR_NUMBER() AS err, ERROR_MESSAGE() AS msg; -- 208, caught
END CATCH
The same rule is why a stored procedure can be created against a table that does not exist but a plain batch cannot. It is the deferred name resolution above, seen from the other side.
Case Sensitivity
On a case-sensitive or binary collation, Orders and orders are two different names, and only one of them exists.
SELECT DATABASEPROPERTYEX(DB_NAME(), 'Collation') AS db_collation;
Anything ending _CS_AS or _BIN2 means the object name has to match exactly. Most installations are case-insensitive, which is why this one gets missed on the servers where it matters.
What Not To Do
Do not “fix” this by creating the missing object. On a case-sensitivity or default-schema problem you will end up with two tables that look identical in Object Explorer, diverging quietly, and the application reading whichever one its connection resolves to. Find the original first.
Common Questions
It works for me but not for my colleague.
schema.object removes the whole class of problem.The procedure created without an error, so how can the table be missing?
It works in SSMS and fails in the application.
DB_NAME() in both, then the connection string: the query is identical, the context is not.Could this be a permissions problem instead?
Permissions do reach you through the back door though: you cannot see an object you have no rights on, so
OBJECT_ID() returns NULL and any tool that probes with it will tell you the object does not exist. Use HAS_PERMS_BY_NAME to tell "absent" apart from "not for you".My editor underlines the table in red but the query runs fine.
Ctrl+Shift+R to refresh it. The server was never involved.Why does my TRY/CATCH not catch 208?
CATCH. Move the reference into sp_executesql so binding happens at execution time, and the outer CATCH will see it.Related Scripts
- Get Database Inventory, confirm which databases exist and what state each is in
- Get User Permissions Audit, one login’s effective access, including its default schema
- Collation and Case Sensitivity, when the name matches but the case does not
Leave a Reply