STATUS_OBJECT_NO_LONGER_EXISTS (0XC0190021) Fix
This error hits when a database cursor or handle points to a deleted row or object. It's common in SQL Server with stale cursors or orphaned tempdb objects.
When you'll see this error
This error slaps you across the face inside SQL Server Management Studio or from a client app like a .NET web service. The exact text reads STATUS_OBJECT_NO_LONGER_EXISTS with hex code 0XC0190021. You'll get it when a cursor tries to fetch a row that's been deleted by another session, or when a stored procedure holds a reference to a temp table that's been dropped by a parallel transaction. I've seen it most often in high-concurrency OLTP systems where multiple processes read and delete from the same table within milliseconds of each other.
The real trigger is a race condition: your code opens a read-only cursor, fetches a few rows, then another session deletes one of those rows before your next fetch completes. On SQL Server, cursors use keyset-driven or dynamic positioning—when you do FETCH NEXT FROM cursor_name, the engine checks if the underlying row still exists in the base table. If it doesn't, you get this error instead of a graceful empty row. It's not a deadlock—it's a straight-up missing data condition that the cursor can't handle.
Root cause in plain English
What's actually happening here is your cursor is a snapshot of a pointer, not the data itself. SQL Server's cursor engine maintains a keyset—a set of unique identifiers (usually the clustered index keys) for the rows the cursor should return. When you fetch, it resolves those keys against the current state of the table. If another transaction deletes that row between the time the cursor was opened and the fetch call, the keyset entry points to nothing. The OS layer underneath SQL Server (Windows NT status codes) translates this as STATUS_OBJECT_NO_LONGER_EXISTS because the underlying storage object—the row's B-tree entry—is gone.
The mistake most devs make is assuming cursors are safe for concurrent read-delete workloads. They're not. Cursors are designed for sequential processing of stable data, not for tables where other sessions are actively deleting rows you haven't fetched yet. The fix isn't to add retry logic—that just masks the problem. You need to change the cursor type or restructure the query to avoid the race.
Step-by-step fix
- Identify which cursor is failing
Run this in SSMS to see all active cursors and their SQL text:
SELECT * FROM sys.dm_exec_cursors(0);
The columnfetch_statuswill show the error state for the problematic cursor. Note thecursor_nameandstatement_text. - Fix the cursor declaration
Change yourDECLARE cursor_name CURSORstatement. Use one of these two options:
DECLARE cursor_name CURSOR STATIC READ_ONLY FOR ...— This takes a full snapshot of the data when opened. No race condition because the cursor works from tempdb, not the live table. Tradeoff: memory and tempdb I/O for large result sets.DECLARE cursor_name CURSOR DYNAMIC SCROLL_LOCKS FOR ...— Locks each row as you fetch it. Prevents deletes from other sessions. Tradeoff: blocks other writers.
STATICfor most read-only scenarios. It's safer and doesn't block writes. - Add error handling in your fetch loop
Wrap theFETCHand subsequent processing in aTRY...CATCHblock. If@@FETCH_STATUSreturns -2 (row not found), skip and continue:
This is a band-aid—better to fix the cursor type above.BEGIN TRY FETCH NEXT FROM my_cursor INTO @var1, @var2; IF @@FETCH_STATUS = -2 CONTINUE; -- process the row END TRY BEGIN CATCH IF ERROR_NUMBER() = 0XC0190021 CONTINUE; ELSE THROW; END CATCH - Drop and recreate the cursor with the new type
DEALLOCATE cursor_name;
Then declare it asSTATICper step 2. Test the fetch loop again. - If you can't change the cursor code
Sometimes you're stuck with a third-party app or legacy stored proc you can't modify. In that case, add aWAITFOR DELAY '00:00:00.100'before the cursor open to let competing transactions finish. Or wrap the entire cursor block inside a serializable transaction (SET TRANSACTION ISOLATION LEVEL SERIALIZABLE) to prevent deletes on the tables involved. This kills concurrency but eliminates the error.
What to check if it still fails
If you changed the cursor to STATIC and still get the error, look at these three things:
- Tempdb corruption — The static cursor stores its data in tempdb. Run
DBCC CHECKDB('tempdb')with no repair options first to see if there's corruption. If tempdb's broken, the cursor snapshot can fail mid-fetch with this same error. Restart SQL Server to clear tempdb, then check disk I/O for underlying storage problems. - Cross-database cursors — If your cursor references tables from different databases, the
STATICcursor might still fail if the source database goes offline briefly or a schema change occurs. In that case, copy the needed data into local temp tables (#temp) before opening the cursor. That isolates you from schema changes. - Application-level retry — If the cursor is opened and closed by your app code, ensure you re-open the cursor after any exception. Don't reuse a deallocated cursor handle. In C#, that means catching
SqlExceptionwith error number 0XC0190021, then disposing the command and recreating it.
One final opinion: if you can avoid cursors entirely, do it. Use set-based UPDATE or DELETE with OUTPUT clauses, or use a recursive CTE. This error is a warning that your design fights the engine. Cursors aren't evil, but they're almost never the right tool for concurrent environments.
Was this solution helpful?