tursodatabase/turso · error · ObjectDisposedException
ScalarFunctionRegistration
Error message
ScalarFunctionRegistration
What it means
The engine invoked a custom scalar function (CreateFunction) through the native trampoline, but the GCHandle for the managed ScalarFunctionRegistration has no live target: the registration was disposed while a statement still references the function. Registrations are torn down when the connection closes (FreeNativeFunctionContexts), so this indicates the connection died before a query calling the UDF finished. It is the scalar-function flavor of use-after-dispose across the interop boundary.
Source
Thrown at bindings/dotnet/src/Turso.Data.Sqlite/SqliteConnection.Functions.cs:102
var targetType = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T);
if (targetType == typeof(object))
return (T)value;
if (targetType == typeof(byte[]) && value is byte[] bytes)
return (T)(object)bytes;
if (targetType == typeof(string))
return (T)(object)Convert.ToString(value, CultureInfo.InvariantCulture)!;
if (targetType == typeof(bool))
return (T)(object)Convert.ToBoolean(value, CultureInfo.InvariantCulture);
return (T)Convert.ChangeType(value, targetType, CultureInfo.InvariantCulture);
}
private static TursoExtensionValue InvokeScalarFunction(IntPtr context, int argc, IntPtr argv, IntPtr contextDestructor, IntPtr valueDestructor)
{
try
{
var registration = (ScalarFunctionRegistration?)GCHandle.FromIntPtr(context).Target
?? throw new ObjectDisposedException(nameof(ScalarFunctionRegistration));
var args = ReadArguments(argc, argv);
return CreateResult(registration.Invoke(args));
}
catch (SqliteException ex)
{
return CreateError("__turso_sqlite_error__:" + ex.SqliteErrorCode.ToString(CultureInfo.InvariantCulture) + ":" + ex.Message);
}
catch (Exception ex)
{
return CreateError(ex.Message);
}
}
private static void NoopContextDestructor(IntPtr context)
{
}
private static void DestroyFunctionValue(IntPtr result)View on GitHub (pinned to 6c72522679)
Solutions
- Keep the connection alive until every reader/command invoking the UDF has completed and been disposed.
- Materialize results (ToList()) before the owning scope disposes the connection.
- Register the function on each connection (CreateFunction is per-connection) and scope its lifetime to that connection's queries.
- Cancel queries via command cancellation rather than disposing the connection.
Example fix
// before
List<int> ids;
using (var conn = new SqliteConnection(cs))
{
conn.Open();
conn.CreateFunction<string, long>("len", s => s.Length);
var cmd = new SqliteCommand("SELECT len(name) FROM t", conn);
ids = ReadLazy(cmd.ExecuteReader()).ToList(); // reader escapes scope
}
// after
using (var conn = new SqliteConnection(cs))
{
conn.Open();
conn.CreateFunction<string, long>("len", s => s.Length);
using var cmd = new SqliteCommand("SELECT len(name) FROM t", conn);
using var reader = cmd.ExecuteReader();
ids = ReadAll(reader); // consumed before conn disposes
} Defensive patterns
Strategy: validation
Validate before calling
static List<T> ReadAll<T>(SqliteCommand cmd, Func<SqliteDataReader, T> map)
{
if (cmd.Connection?.State != ConnectionState.Open)
throw new InvalidOperationException("Connection must be open before invoking UDF queries.");
using var reader = cmd.ExecuteReader();
var results = new List<T>();
while (reader.Read()) results.Add(map(reader));
return results; // fully consumed before caller can dispose the connection
} Try / catch
try
{
var rows = ReadAll(cmd, MapRow);
}
catch (ObjectDisposedException ex) when (ex.ObjectName == "ScalarFunctionRegistration")
{
throw new InvalidOperationException("UDF query outlived its connection; re-run on a fresh connection.", ex);
} Prevention
- Scope 'using var conn' outside every 'using var cmd' / 'using var reader' that calls UDFs.
- Materialize deferred sequences before leaving the connection's scope.
- Register functions per connection; do not assume registrations survive close/open.
When it happens
Trigger: Closing/disposing the SqliteConnection while a reader for a query that calls the UDF is still open; deferred LINQ evaluation escaping the connection's using scope; one thread disposing the connection while another executes a UDF query; long-running queries cancelled by tearing down the connection.
Common situations: 'using var conn' in a repository method returning a lazy sequence, unit tests that dispose fixtures while background queries run, and DI lifetimes disposing shared connections mid-request.
Related errors
- Cannot access a disposed object. Object name: 'AggregateInv
- CollationRegistration
- Function {function} was called with a NULL value at ordinal
- Encryption is not supported by {libraryName}.
- Missing parameter values for {parameters}.
AI-assisted analysis of tursodatabase/turso@6c72522679 (2026-08-20).
Data as JSON: /api/errors/9c32cc954e54bcd1.
Report an issue: GitHub.