I ran into a weird behavior in exceptional Dart Futures yesterday. In the simplest case, the following works:
main() {
var completer = new Completer();
var future = completer.future;
future.handleException((e) {
print("Handled: $e");
return true;
});
completer.completeException("That ain't gonna work");
}
(try.dartlang.org)In this case, the completer completes with an exception, not with the normal successful
complete()
. But, since the future associated with the completer injects a handleException()
method, the exception is handled. More specifically since the injected handleException()
returns true, the exception is handled. If I remove the
true
return, I see both the "Handled"
print()
statement as well as the exception bubbling all the way up:Handled: That ain't gonna work Exception: That ain't gonna work Stack Trace: 0. Function: 'FutureImpl._complete@924b4b8' url: 'bootstrap_impl' line:3124 col:9 1. Function: 'FutureImpl._setException@924b4b8' url: 'bootstrap_impl' line:3146 col:14 2. Function: 'CompleterImpl.completeException' url: 'bootstrap_impl' line:3207 col:30 3. Function: '::main' url: 'file:///home/cstrom/repos/dart-book/book/includes/no_more_callback_hell/main.dart' line:12 col:30(try.dartlang does not seem to care about the return value—it always considers
handleException()
to have handled the exception)My problem is that, although this behaves as expected in a simple case, it does not seem to work as expected in a more complex case. In my Hipster MVC library, my syncing layer that is responsible for coordinating in-browser data with permanent storage can have different behavior injected. So the
HipsterSync.call()
class method first needs a conditional to return a Future from the appropriate behavior (_defultSync in this case):class HipsterSync { // ... static Future<Dynamic> call(method, model) { if (_injected_sync == null) { return _defaultSync(method, model); } else { /* ... */ } } // ... }The default syncing behavior then creates the completer and invokes
completeException()
when a non-200 response is received from the backend:class HipsterSync { // ... static Future<Dynamic> _defaultSync(method, model) { var request = new XMLHttpRequest(), completer = new Completer(); request. on. load. add((event) { var req = event.target; if (req.status > 299) { completer. completeException("That ain't gonna work: ${req.status}"); } else { /* ... */ } }); // ... return completer.future; } }Finally, the
HipsterModel
class is invoking HipsterSync.call()
and trying to do the right thing with handleException()
:class HipsterCollection implements Collection { // ... create(attrs) { Future after_save = _buildModel(attrs).save(); after_save. then((saved_model) { this.add(saved_model); }); after_save. handleException(bool (e) { print("Exception handled: ${e.type}"); return true; }); } // ... }When I generate a 409, however, that
handleException()
does not kick in. Instead, I see the exception bubbling all the way up to the Dart console:POST http://localhost:3000/comics 409 (Conflict) Exception: That ain't gonna work: 409 Stack Trace: 0. Function: 'FutureImpl._complete@924b4b8' url: 'bootstrap_impl' line:3124 col:9 1. Function: 'FutureImpl._setException@924b4b8' url: 'bootstrap_impl' line:3146 col:14 2. Function: 'CompleterImpl.completeException' url: 'bootstrap_impl' line:3207 col:30 3. Function: 'HipsterSync.function' url: 'http://localhost:3000/scripts/HipsterSync.dart' line:44 col:30 4. Function: 'EventListenerListImplementation.function' url: 'dart:htmlimpl' line:23163 col:35Seemingly, the problem lies somewhere in between the simplest use-case and my complex Hipster MVC. So I start expanding from this simple case to more and more complex cases. After a time, I have worked through no less than six test cases on increasing complexity—all of them working. In my last test case, I have a static method from one class produce a completer exception that another class handles. Both are in separate libraries.
They say you can't prove a negative, but I seem to have just done so.
So I go back to my code and, indeed, the bug was mine. I eventually realize that the
Future
in HipsterCollection
is not coming from HipsterSync
as I thought. Rather it comes from HipsterModel
:class HipsterCollection implements Collection { // ... create(attrs) { Future after_save = _buildModel(attrs).save(); // ... } }In hindsight this seems perfectly obvious.
When I first refactored into Completers and Futures, I did take into account that
HipsterModel
required changes as well:class HipsterModel { // ... Future<HipsterModel> save() { Completer completer = new Completer(); HipsterSync. call('post', this). then((attrs) { this.attributes = attrs; on.load.dispatch(new ModelEvent('save', this)); completer.complete(this); }); return completer.future; } }What is missing from that implementation is concern about an exception from
HipsterSync.call()
. The only thing that I do is wait patiently for a then()
that never comes. So, just as I do in the HipsterCollection
, I grab the Future returned by HipsterSync.call()
and inject then()
and handleException()
behavior:class HipsterModel {
// ...
Future<HipsterModel> save() {
Completer completer = new Completer();
Future after_call = HipsterSync.call('post', this);
after_call.
then((attrs) {
this.attributes = attrs;
on.load.dispatch(new ModelEvent('save', this));
completer.complete(this);
});
after_call.
handleException((e) {
completer.completeException(e);
return true;
});
return completer.future;
}
}
It feels a bit much having to completeException()
/ handleException()
all the way from HipsterSync, through HipsterModel and on up to HipsterCollection. Then again, the model has need to perform error handling before notifying the collection that something went wrong, so I see no real way around this.At any rate, the Future exception is now captured in the model, re-raisd to the collection in such a way that the collection can handle it:
It is difficult when working with new languages to judge whether a mistake like this lies with the language (e.g. too much complexity) or myself. In this case, my understanding of the technique was solid, but I muffed the implementation—was that Dart's fault or my own? I think in this case, it was my own. I think the architecture that I have chosen necessitates this type of propagation on occasion and I need to be cognizant of this. Re-examining my ultimate solution, I do not see anywhere that Dart could have eased my burden. Then again, perhaps I simply lack imagination.
Day #315
No comments:
Post a Comment