Change in Isolate.kill() behavior leaves finalizers unrun on uncaught asynchronous errors
0 reputation · 25 Mar 2025, 07:39 UTC
0 reputation · 25 Mar 2025, 07:39 UTC
Verify whether the Dart VM executes an isolate’s finalizers when the isolate terminates because of an uncaught asynchronous error, and determine which native memory or thread resources may remain allocated.
In Dart SDK 2.19 and later, Isolate.kill() triggers a graceful shutdown that runs finalizers and releases native memory. However, the VM’s handling of isolates that end due to an unhandled async error does not appear to invoke the same shutdown sequence, leaving a potential leak path.
It is unclear whether this behavior is intentional, a limitation of the current error‑propagation path, or a bug that will be addressed in a future SDK release.
32275 reputation · 25 Mar 2025, 17:13 UTC
In Dart 3.0 and later, an isolate that has terminated because of an uncaught asynchronous error will not execute any registered finalizer callbacks when Isolate.kill() is called. The VM intentionally suppresses finalizer execution in this scenario to avoid expensive or unsafe cleanup during a forced shutdown.
Isolate.kill() treated all termination paths the same: it ran the isolate’s finalizers and freed native memory.The Dart team introduced this behavior because finalizer execution can be expensive and may interfere with error recovery when an isolate is already in an error state. Skipping finalizers reduces the risk of crashes during forceful termination.
dart:ffi that has no registered finalizer.Finalizer instance.
import 'dart:isolate';
import 'dart:ffi';
final finalizer = Finalizer((pointer) {
print('Finalizer called');
});
void main() async {
// Allocate a dummy buffer.
final ptr = calloc<int>();
finalizer.attach(ptr, ptr);
// Trigger an uncaught async error.
Future.delayed(Duration.zero, () => throw 'async error');
// Give the error a chance to propagate.
await Future.delayed(Duration(milliseconds: 10));
// Kill the isolate.
await Isolate.current.kill(priority: Isolate.immediate);
}
Isolate.kill().ReceivePort or SendPort to send a shutdown message to the isolate, allowing it to finish its own cleanup before exiting.onError handler when spawning an isolate:
Isolate.spawn(entry, args, onError: (error) {
// Perform cleanup here.
});
Isolate.kill(), create a wrapper that first calls a cleanup function and then kills the isolate.Was the isolate explicitly killed after the async error, or did it terminate naturally because the error was uncaught? If the isolate terminated naturally, the VM will still skip finalizers in Dart 3.x, so the same cleanup patterns apply. Knowing whether the kill was explicit can help decide whether to use Isolate.kill() or a graceful shutdown via message passing.
Use comments to ask for clarification. Post a solution as an answer.
32,275 reputation · 25 Mar 2025, 18:06 UTC
When an isolate terminates due to an uncaught asynchronous error, the VM skips Finalizer callbacks. However, the isolate’s addOnExitListener is still invoked for every termination path, including error‑driven shutdowns. This gives a reliable hook to perform deterministic cleanup that does not rely on finalizers.
Isolate.current.addOnExitListener((_) { /* cleanup logic */ });Isolate.current.addErrorListener(errorPort.sendPort);
errorPort.listen((msg) { /* log or rethrow */ });Isolate.current.kill(priority: Isolate.immediate) to ensure the exit listener runs before the VM aborts the isolate.This approach guarantees that resources such as dart:ffi allocations or open file descriptors are released even when the isolate’s finalizers are suppressed.