diff --git a/lib/ui/fixtures/ui_test.dart b/lib/ui/fixtures/ui_test.dart index ad560db96ba30..3fd410d49a450 100644 --- a/lib/ui/fixtures/ui_test.dart +++ b/lib/ui/fixtures/ui_test.dart @@ -747,9 +747,98 @@ void hooksTests() { expectEquals(frameNumber, 2); }); + test('_futureize handles callbacker sync error', () async { + String? callbacker(void Function(Object? arg) cb) { + return 'failure'; + } + Object? error; + try { + await _futurize(callbacker); + } catch (err) { + error = err; + } + expectNotEquals(error, null); + }); + + test('_futureize does not leak sync uncaught exceptions into the zone', () async { + String? callbacker(void Function(Object? arg) cb) { + cb(null); // indicates failure + } + Object? error; + try { + await _futurize(callbacker); + } catch (err) { + error = err; + } + expectNotEquals(error, null); + }); + + test('_futureize does not leak async uncaught exceptions into the zone', () async { + String? callbacker(void Function(Object? arg) cb) { + Timer.run(() { + cb(null); // indicates failure + }); + } + Object? error; + try { + await _futurize(callbacker); + } catch (err) { + error = err; + } + expectNotEquals(error, null); + }); + + test('_futureize successfully returns a value sync', () async { + String? callbacker(void Function(Object? arg) cb) { + cb(true); + } + final Object? result = await _futurize(callbacker); + + expectEquals(result, true); + }); + + test('_futureize successfully returns a value async', () async { + String? callbacker(void Function(Object? arg) cb) { + Timer.run(() { + cb(true); + }); + } + final Object? result = await _futurize(callbacker); + + expectEquals(result, true); + }); + _finish(); } +typedef _Callback = void Function(T result); +typedef _Callbacker = String? Function(_Callback callback); + +// This is an exact copy of the function defined in painting.dart. If you change either +// then you must change both. +Future _futurize(_Callbacker callbacker) { + final Completer completer = Completer.sync(); + // If the callback synchronously throws an error, then synchronously + // rethrow that error instead of adding it to the completer. This + // prevents the Zone from receiving an uncaught exception. + bool sync = true; + final String? error = callbacker((T? t) { + if (t == null) { + if (sync) { + throw Exception('operation failed'); + } else { + completer.completeError(Exception('operation failed')); + } + } else { + completer.complete(t); + } + }); + sync = false; + if (error != null) + throw Exception(error); + return completer.future; +} + void _callHook( String name, [ int argCount = 0, diff --git a/lib/ui/painting.dart b/lib/ui/painting.dart index 9f6763f206a99..018dfd7cfcc97 100644 --- a/lib/ui/painting.dart +++ b/lib/ui/painting.dart @@ -5599,15 +5599,27 @@ typedef _Callbacker = String? Function(_Callback callback); /// return _futurize(_doSomethingAndCallback); /// } /// ``` +// Note: this function is not directly tested so that it remains private, instead an exact +// copy of it has been inlined into the test at lib/ui/fixtures/ui_test.dart. if you change +// this function, then you must update the test. Future _futurize(_Callbacker callbacker) { final Completer completer = Completer.sync(); + // If the callback synchronously throws an error, then synchronously + // rethrow that error instead of adding it to the completer. This + // prevents the Zone from receiving an uncaught exception. + bool sync = true; final String? error = callbacker((T? t) { if (t == null) { - completer.completeError(Exception('operation failed')); + if (sync) { + throw Exception('operation failed'); + } else { + completer.completeError(Exception('operation failed')); + } } else { completer.complete(t); } }); + sync = false; if (error != null) throw Exception(error); return completer.future; diff --git a/lib/web_ui/lib/initialization.dart b/lib/web_ui/lib/initialization.dart index be7572cedcccc..4c2c81f0183cd 100644 --- a/lib/web_ui/lib/initialization.dart +++ b/lib/web_ui/lib/initialization.dart @@ -139,15 +139,28 @@ final PlatformViewRegistry platformViewRegistry = PlatformViewRegistry(); // NNBD migration. typedef _Callback = void Function(T result); typedef _Callbacker = String? Function(_Callback callback); + +// Note: this function is not directly tested so that it remains private, instead an exact +// copy of it has been inlined into the test at lib/ui/fixtures/ui_test.dart. if you change +// this function, then you must update the test. Future _futurize(_Callbacker callbacker) { final Completer completer = Completer.sync(); - final String? error = callbacker((T t) { + // If the callback synchronously throws an error, then synchronously + // rethrow that error instead of adding it to the completer. This + // prevents the Zone from receiving an uncaught exception. + bool sync = true; + final String? error = callbacker((T? t) { if (t == null) { - completer.completeError(Exception('operation failed')); + if (sync) { + throw Exception('operation failed'); + } else { + completer.completeError(Exception('operation failed')); + } } else { completer.complete(t); } }); + sync = false; if (error != null) { throw Exception(error); }