tursodatabase/turso · error · InvalidOperationException

{method} requires an open data reader.

Error message

{method} requires an open data reader.

What it means

InvalidOperationException thrown by EnsureOpen when any reader member is called after the reader is closed. The message interpolates the calling member ('{method} requires an open data reader.'), with get_ prefixes stripped so it reads like 'Read requires an open data reader.' or 'GetInt32 requires an open data reader.'. Close/Dispose sets _isClosed, and every data/metadata accessor funnels through this guard.

Source

Thrown at bindings/dotnet/src/Turso.Data.Sqlite/SqliteDataReader.cs:720

        FinishClose();
    }

    private void FinishClose()
    {
        if (_isClosed)
            return;

        _closeCallback();
        if ((_behavior & CommandBehavior.CloseConnection) == CommandBehavior.CloseConnection)
            _command.Connection?.Close();

        _isClosed = true;
    }

    private void EnsureOpen([CallerMemberName] string operation = "")
    {
        if (IsClosed)
            throw new InvalidOperationException(Properties.Resources.DataReaderClosed(NormalizeOperationName(operation)));
    }

    private static string NormalizeOperationName(string operation)
        => operation.StartsWith("get_", StringComparison.Ordinal)
            ? operation[4..]
            : operation;

    private TursoStatementHandle GetStatement()
    {
        if (_statement is null)
            throw new InvalidOperationException(Properties.Resources.NoData);

        return _statement;
    }

    private void ValidateOrdinal(int ordinal)
    {
        if (ordinal < 0 || ordinal >= FieldCount)

View on GitHub (pinned to 244cde92a7)

Solutions

  1. Keep all reads inside the using scope that owns the reader, and materialize (ToList) before leaving it when data must escape.
  2. Guard public helper APIs with if (reader.IsClosed) throw ... to fail with your own clear message instead.
  3. Return disconnected data (records/DTOs) from data-access methods instead of the live reader.

Example fix

// before
SqliteDataReader ReadAll() {
    using var reader = cmd.ExecuteReader();
    return reader; // caller uses it after dispose
}

// after
List<Row> ReadAll() {
    using var reader = cmd.ExecuteReader();
    var rows = new List<Row>();
    while (reader.Read()) rows.Add(new Row(reader.GetInt32(0)));
    return rows; // data survives reader disposal
}
Defensive patterns

Strategy: validation

Validate before calling

if (reader.IsClosed) throw new InvalidOperationException("Reader already closed; re-execute the query.");
var v = reader.GetInt32(0);

Try / catch

try { Process(reader); }
catch (InvalidOperationException ex) when (ex.Message.Contains("requires an open data reader"))
{ /* re-run the query and retry processing once */ }

Prevention

When it happens

Trigger: Accessing reader.Read(), GetName(i), GetInt32(i), FieldCount, etc. after Close() or after leaving the using block; storing the reader beyond the command lifetime (e.g. returning it from a method that disposed the command); double-enumerating a deferred LINQ result over a closed reader.

Common situations: Wrapping the reader in a method-scoped using but returning data lazily (IEnumerable yield over a disposed reader); sharing a reader with async code that completes after disposal; helper methods receiving a reader whose owner already closed it.

Related errors


AI-assisted analysis of tursodatabase/turso@244cde92a7 (2026-08-20). Data as JSON: /api/errors/201cf075a54d12b4. Report an issue: GitHub.