tursodatabase/turso · error · NullReferenceException

statement is invalid

Error message

statement is invalid

What it means

TursoStatementHandle guards a single native statement pointer. ThrowIfInvalid throws NullReferenceException when that pointer is zero — i.e., after ReleaseHandle/StatementFinalize ran or the handle was never bound. Every statement API (BindParameter, Read, GetValue, GetName, RowsAffected, GetParameterCount) calls it first.

Source

Thrown at bindings/dotnet/src/Turso.Raw/Public/Handles/TursoStatementHandle.cs:21

namespace Turso.Raw.Public.Handles;

public class TursoStatementHandle() : SafeHandle(IntPtr.Zero, true)
{
    protected override bool ReleaseHandle()
    {
        _ = TursoInterop.StatementFinalize(handle, out var errorPtr);
        if (errorPtr != IntPtr.Zero)
            TursoInterop.FreeString(errorPtr);

        TursoInterop.StatementDeinit(handle);
        handle = IntPtr.Zero;
        return true;
    }

    public void ThrowIfInvalid()
    {
        if (IsInvalid)
            throw new NullReferenceException("statement is invalid");
    }

    public static TursoStatementHandle FromPtr(IntPtr ptr)
    {
        var handle = new TursoStatementHandle();
        handle.SetHandle(ptr);
        return handle;
    }

    public override bool IsInvalid => handle == IntPtr.Zero;

}

View on GitHub (pinned to 244cde92a7)

Solutions

  1. Tie statement lifetime to the connection: dispose statements before the database handle closes (e.g. nested usings).
  2. Check statement.IsInvalid before reuse, and re-prepare from a valid database handle when true.
  3. Never share one statement across concurrent operations; prepare per scope.
  4. Return values, not statements, from helpers so callers cannot hold dead handles.

Example fix

// before
var stmt = TursoBindings.PrepareStatement(db, sql);
stmt.Dispose();
TursoBindings.Read(stmt); // NullReferenceException: statement is invalid

// after
using var stmt = TursoBindings.PrepareStatement(db, sql);
while (TursoBindings.Read(stmt))
{
    var value = TursoBindings.GetValue(stmt, 0);
}
Defensive patterns

Strategy: type-guard

Validate before calling

// check before every use of the statement handle
if (stmt.IsInvalid)
    stmt = TursoBindings.PrepareStatement(db, sql);

Type guard

static bool IsUsable(TursoStatementHandle s) => s is not null && !s.IsInvalid;

Try / catch

try { TursoBindings.Read(stmt); }
catch (NullReferenceException) when (stmt.IsInvalid) { stmt = TursoBindings.PrepareStatement(db, sql); /* re-run */ }

Prevention

When it happens

Trigger: Calling any statement API after finalizing/disposing the statement; stepping or binding a statement that was prepared on a database handle that has since been closed; double-dispose where the second user still holds the reference.

Common situations: Statements cached beyond the owning connection's lifetime; concurrent code paths where one thread finalizes while another reads; helper wrappers that dispose statements eagerly inside a using block that callers escape via captured references.

Related errors


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