0XC0190021

STATUS_OBJECT_NO_LONGER_EXISTS (0XC0190021) Fix

Database Errors Intermediate 👁 0 views 📅 May 26, 2026

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

  1. 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 column fetch_status will show the error state for the problematic cursor. Note the cursor_name and statement_text.
  2. Fix the cursor declaration
    Change your DECLARE cursor_name CURSOR statement. 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.
    I recommend STATIC for most read-only scenarios. It's safer and doesn't block writes.
  3. Add error handling in your fetch loop
    Wrap the FETCH and subsequent processing in a TRY...CATCH block. If @@FETCH_STATUS returns -2 (row not found), skip and continue:
    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
    This is a band-aid—better to fix the cursor type above.
  4. Drop and recreate the cursor with the new type
    DEALLOCATE cursor_name;
    Then declare it as STATIC per step 2. Test the fetch loop again.
  5. 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 a WAITFOR 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 STATIC cursor 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 SqlException with 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?