Repository: flutter
Issue: The `onError` argument of `SynchronousFuture.then` is unused
Labels: c: crash,framework,has reproducible steps,P2,found in release: 3.3,found in release: 3.7,team-framework,triaged-framework

Encountered while working on #113637.
Tangentially related: https://github.com/flutter/flutter/issues/115601.

See title.

https://github.com/flutter/flutter/blob/c0719509a1603a2357996b38ce94397f10ac3957/packages/flutter/lib/src/foundation/synchronous_future.dart#L39-L46

Callers of a function that can return a `SynchronousFuture` can be surprised that the `onError` callback they pass to `then` is not invoked in the case that either
1. the called function throws, or
2. the `then` callback throws.

While case 1 is not directly fixable[^1], we could do better in case 2.

## Suggestions for fixing case 2
1. Within the implementation of `SynchronousFuture.then`, if `onError` is non-null, catch errors thrown by the `onValue` callback and invoke `onError`. However, with this we go from the confusing situation of `onError` never working (for both case 1 and 2) to the confusion of `onError` only working in some scenarios (case 2), which I'm not sure is much better.
2. From `SynchronousFuture.then`, print a warning if the `onError` argument is observed to be non-null. `the onError argument cannot catch errors originating from a callback of a SynchronousFuture` could be a decent point to start from. edit: I don't think this is a good idea, because such a warning is likely to be unactionable by the end user (unless they are working on flutter itself).
3. (Not mutually exclusive) Add a more specific note warning of this behavior to the docstring of `SynchronousFuture`.

## Toy repro code

[Repro project repo](https://github.com/andrewkolos/repro_flutter_sync_future_then_onerror_arg)

<details>

<summary>main.dart</summary>

```dart
void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key});

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  String futureStatus = 'loading...';

  @override
  void initState() {
    super.initState();

    // Flutter version: master (cacf1d298f)
    // Expected outcome: `then` is entered synchronously,
    // _dangerousOperation throws, the `onError` callback
    // is invoked, and the UI shows a message telling
    // us that the operation completed with an error.
    // Actual outcome: `then` is entered synchronously,
    // _dangerousOperation throws, but the `onError` callback
    // is never invoked, and the UI is stuck in a "loading" state.
    _safeSyncOrAsyncOperation().then((value) async {
      await _dangerousOperation();
      futureStatus = 'operation completed without error';
    }, onError: (err, stack) {
      futureStatus = 'operation completed with an error';
    });
  }

  // Imagine this is an operation that's normally sync,
  // but could sometimes be async (e.g. cache got invalidated).
  Future<String> _safeSyncOrAsyncOperation() {
    return SynchronousFuture<String>('value');
  }

  Future<String> _dangerousOperation() {
    throw 'how could this happen?!';
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Widget title :)'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text('futureStatus: $futureStatus'),
          ],
        ),
      ),
    );
  }
}
```
</details>

[^1]: Since a `SynchronousFuture` can resolve synchronously, the error would happen before `then` gets invoked.