tursodatabase/turso · error · InvalidOperationException

The data reader is closed.

Error message

The data reader is closed.

What it means

TursoDataReader guards every data-access member with EnsureOpen(), which throws this InvalidOperationException once the reader is closed or disposed. It is the ADO.NET-standard 'object used after Close/Dispose' signal: the native statement handle behind the reader is gone, so reads are impossible.

Source

Thrown at bindings/dotnet/src/Turso.Data/TursoDataReader.cs:322

    private static string GetTypeName(TursoValueType valueType)
    {
        return valueType switch
        {
            TursoValueType.Empty => "",
            TursoValueType.Null => "NULL",
            TursoValueType.Integer => "INTEGER",
            TursoValueType.Real => "REAL",
            TursoValueType.Text => "TEXT",
            TursoValueType.Blob => "BLOB",
            _ => throw new InvalidEnumArgumentException(nameof(valueType))
        };
    }

    private void EnsureOpen()
    {
        if (IsClosed)
            throw new InvalidOperationException("The data reader is closed.");
    }

    private void RunExternalIo()
    {
        _syncConnection?.RunExternalIo();
    }

    private void ValidateOrdinal(int ordinal)
    {
        ArgumentOutOfRangeException.ThrowIfNegative(ordinal);
        if (ordinal >= FieldCount)
            throw new IndexOutOfRangeException($"column ordinal {ordinal} is out of range");
    }
}

internal static class DataReaderCompatibility
{
    public static DataTable CreateSchemaTable(DbDataReader reader)

View on GitHub (pinned to 6c72522679)

Solutions

  1. Materialize results while the reader is alive: call ToList() inside the using block before returning.
  2. Check 'reader.IsClosed' before touching the reader in long-lived or callback code.
  3. Keep the reader's lifetime nested inside the command's and the connection's lifetime.

Example fix

// before
using var reader = cmd.ExecuteReader();
return MapRows(reader); // MapRows is lazy and enumerates after dispose

// after
using var reader = cmd.ExecuteReader();
var rows = new List<Row>();
while (reader.Read()) rows.Add(MapRow(reader));
return rows;
Defensive patterns

Strategy: validation

Validate before calling

if (reader is { IsClosed: false })
    value = reader.GetString(ordinal);

Try / catch

try { rows = MapAll(reader); } catch (InvalidOperationException ex) when (ex.Message == "The data reader is closed.") { /* lifetime bug: materialize before dispose */ }

Prevention

When it happens

Trigger: Calling any Get* method, Read(), GetOrdinal(), or typed accessors after Close()/Dispose(), after the owning connection was closed, after the 'using' block ended, or from a deferred IEnumerable/LINQ query that enumerates the reader after it has been disposed.

Common situations: Returning a lazy projection from a using block (e.g. 'return rows.Select(r => Map(r));' inside 'using var reader'), sharing a reader with async code that outlives its scope, event handlers firing after the reader was closed by error handling.

Related errors


AI-assisted analysis of tursodatabase/turso@6c72522679 (2026-08-31). Data as JSON: /api/errors/8f4cbd04c8c3b24e. Report an issue: GitHub.