Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bridging Dart Exceptions with Isolates: Addressing the Incompatibility #59608

Open
stephane-archer opened this issue Nov 26, 2024 · 6 comments
Open
Labels
area-core-library SDK core library issues (core, async, ...); use area-vm or area-web for platform specific libraries. library-async type-enhancement A request for a change that isn't a bug

Comments

@stephane-archer
Copy link

When exploring the Dart standard library, it becomes evident that exceptions are the primary mechanism for handling errors. This approach works seamlessly in most scenarios—until you introduce isolates into the equation. Isolates rely on values to handle errors, creating a dissonance between the two paradigms.

This mismatch forces developers to write wrapper functions to convert exceptions into value-based errors to make them compatible with isolates. This process can be tedious and introduces unnecessary complexity, especially when working with existing Dart codebases.

A potential improvement would be enabling isolates to propagate exceptions back to the sender. This enhancement could significantly reduce the friction caused by this inconsistency, allowing for a more harmonious interaction between isolates and Dart's standard error-handling mechanisms.

Currently, this disconnect makes it cumbersome to leverage isolates effectively within Dart, particularly in projects that depend on exception-driven error handling.

@dart-github-bot
Copy link
Collaborator

Summary: Dart isolates don't directly support exception propagation, requiring manual conversion to values. This adds complexity when bridging isolates and exception-based error handling.

@dart-github-bot dart-github-bot added area-vm Use area-vm for VM related issues, including code coverage, and the AOT and JIT backends. triage-automation See https://github.com/dart-lang/ecosystem/tree/main/pkgs/sdk_triage_bot. type-enhancement A request for a change that isn't a bug labels Nov 26, 2024
@lrhn
Copy link
Member

lrhn commented Nov 26, 2024

Isolates rely on values to handle errors, creating a dissonance between the two paradigms.

Isolates rely on sending objects (errors are objects too) to communicate anything between different isolates.
Exceptions are control flow based. you throw them and they unwind the control flow stack until they are handled, or until reaching the top level where they too are treated as objects. (Asynchronous errors are also just objects until they are re-raised using an await.)
Different isolates do not share any control flow, so they can't communicate using thrown objects. So, yes, isolates communicate objects, by necessity.

What do you suggest to do?

The current Isolate.run does communicate errors back as asynchronous errors in the returned future.
To do that, it's set up to terminate the isolate on the first uncaught error, because it can only report one error.

It's definitely possible to run a computation that returns a Stream in a different isolate and communicate back the events and errors. (The biggest question is whether to use messages or Isolate.pause/Isolate.kill for push-back, the rest is basic message shuffling.)

Which operations, or primitives, are you missing?

@lrhn lrhn added library-isolate and removed triage-automation See https://github.com/dart-lang/ecosystem/tree/main/pkgs/sdk_triage_bot. labels Nov 26, 2024
@stephane-archer
Copy link
Author

@lrhn I have limited knowledge of Isolate so I might be doing something wrong.

I have a function Fibonacci that can throw errors.
I have a list of numbers that are all messages for Fibonacci
I want all of these computations to run in parallel so on different Isolte

List<Future> futures;
for (var message in messages) {
   var future = compute(Fibonacci, message);
   futures.add(future);
}
try {
results = Future.wait(futures)
} catch (e) {
    print("I lost everything if one fail!") 
}

from my understanding if one of the Fibonacci calls throw I lose everything
I don't know any good way to await multiple futures and have a list of results and a list of failures on the other side.

My really ugly solution is wrapping Fibonacci into:

(int result, String error) FibonachiWithoutException(int message) {
try {
     return Fibonachi(message), null
} catch(e) {
   return 0, e.toString();
}
}

so I have a list of results and I can check if it's an error or a value (really golang style)

removing the need for FibonachiWithoutException is what I want

@lrhn
Copy link
Member

lrhn commented Nov 27, 2024

This doesn't seem to be about isolates, just how to combine multiple synchronous results.

You have a number of futures, where some may contain errors, and you want to combine those into a single result, somehow, that represents all of the results.

If you change await Future.wait(futures) to await futures.wait then you're a big step closer. The .wait extension operation on a List<Future<T>> will return a Future<List<T>> with all the results if nothing throws, or will otherwise throw a ParallelWaitError<List<T?>, List<AsyncError?>> which will contain the results split into errors and values (with a null at a position if the result of that future is in the other list).

Then you have all the results, and can decide how you want to continue with those.

A more direct and Dart-like version of the Go-style would be to use the Result class from package:async, and the Result.capture function.
Instead of collecting the computed futures in a list, add Result.capture(future) to the list instead. Then do await results.wait/await Further.wait(results), shouldn't matter which since those Future<Result>s will never throw, and you can then go through the list and ask each Result whether it's a value or an error.

@stephane-archer
Copy link
Author

@lrhn thanks for your great answer!

This doesn't seem to be about isolates, just how to combine multiple synchronous results.

await compute(Fibonacci, message); would result in no parallelism regarding computing values.
And from my understanding, because of exceptions, you should always await at the same time, all the future you have access to.

Because of this, I came up with this solution, but it's more about, "How to compute multiple values in parallel using Dart Isolate?"

I don't care about having all the results all at once, I would much prefer to get their results as soon as they are finished (with some clue of which message has been sent to them preferably) so I can show a progress indication for example or move to the next steps for those fast to compute values.

Do you see any more relevant way to compute multiple values in parallel?
I imagine some streams might be useful.

@lrhn
Copy link
Member

lrhn commented Nov 27, 2024

You'd have the same issue here if compute started asynchronous computations in the same isolate, that run by interleaving instead of concurrently. You have a number of futures created at the same time, where you need to listen to the futures immediately (that's correct), and want to get the results when they're ready.
That's why I said this is not related to using isolates, it doesn't matter where the futures come from. (And that's good, it means any solution can be used generally. Isolates is just a way to get concurrent computations, but the result management is the same as for any other set of asynchronous computations.)

I would indeed use a steam for a set of asynchronous results where the order doesnt matter.
You get a number of asynchronous results, and you need to pass them somewhere when they arrive.
There are many, many ways to do that, but callbacks is the classic one, and Stream is the slightly more modern approach to abstracting over the callback of a number of homogenous asynchronous results.

You could also accumulate the results in a collection, and send a notification when the collection changes.

// Nothing to do on `.onListen`/`onCancel`/`onPause`/`onResume`, can't stop the computations anyway.
var controller = StreamController<int>(sync: true);
var onValue = controller.add;
var onError = controller.addError;
for (var message in messages) {
   var future = compute(Fibonacci, message);
   future.then(onValue, onError: onError);
}
var results = controller.stream;

and then listen to that stream to get the results:

/// ...
results.listen((v) {
  print("Fibonacci Result: $v");
}, onError: (e, s) {
  print("Fibonacci Error: $e\n$s");
}, onDone: () {
  // do something here?
});

You still have to handle each error as it comes in, so you can't just do await for (var value in results) .... That would also break at the first error.

If you need to know the message as well as the result, since you can't rely on order any more, you can do:

  var controller = StreamController<(int message, int result)>(sync: true);    
  var onError = controller.addError;
  for (var message in messages) {
    var future = compute(Fibonacci, message);
    future.then((result) {
      controller.add((message, result));
    }, onError: onError);
  }

And again, you can use Result to convert an asynchornous result-or-error into a value that contains either a result or an error:

import "package:async/async.dart";

// ...
  await for (var result in Result.captureStream(results)) {
    switch (result) {
      case ValueResult(value: (var message, var result)):
        print("Fibonacci($message) = $result");
      case ErrorResult(:var error, :var stackTrace):
        print("Fibonacci Error: $error\n$stackTrace");
    }
  }

@lrhn lrhn added library-async library-core area-core-library SDK core library issues (core, async, ...); use area-vm or area-web for platform specific libraries. and removed area-vm Use area-vm for VM related issues, including code coverage, and the AOT and JIT backends. library-isolate library-core labels Nov 27, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-core-library SDK core library issues (core, async, ...); use area-vm or area-web for platform specific libraries. library-async type-enhancement A request for a change that isn't a bug
Projects
None yet
Development

No branches or pull requests

3 participants