Flutter exception not shown in console

1,551

Solution 1

This ended up being an interesting hunt.

The onSubmitted method of a TextField (as well as the onFieldSubmitted method of a TextFormField) are called from the performAction method of the EditableText base class, which is ultimately called as the result of a platform channel message (specifically the message that says "I'm done editing this text field now"). Within the MethodChannel class, the method responsible for passing the event to the widget is the _handleAsMethodCall method:

Future<ByteData> _handleAsMethodCall(ByteData message, Future<dynamic> handler(MethodCall call)) async {
  final MethodCall call = codec.decodeMethodCall(message);
  try {
    return codec.encodeSuccessEnvelope(await handler(call));
  } on PlatformException catch (e) {
    return codec.encodeErrorEnvelope(
      code: e.code,
      message: e.message,
      details: e.details,
    );
  } on MissingPluginException {
    return null;
  } catch (e) {
    return codec.encodeErrorEnvelope(code: 'error', message: e.toString(), details: null);
  }
}

As you can see, the method call is wrapped in a try/catch which absorbs the errors and returns them to the platform as error responses rather than allowing the error to bubble up naturally. The consequence of this is that the error message is handled by the internal BinaryMessenger rather than by Dart itself. From there it ends up at the Window._respondToPlatformMessage method, which is a native-bound method and an investigatory dead-end (unless you have knowledge of Flutter's platform-specific native implementation).

I'm not sure if this is intended behavior or a bug, but in either case, the result seems to be that the error gets absorbed by the platform/method channel system. I'm crafting an issue for this on the Flutter GitHub page, and will update this answer with the link when the issue is posted.

EDIT: The issue page is here.

Solution 2

You can file this as an issue at the Flutter repo if you want.

Here's a basic explanation on how it works.

If you look at the source code, RaisedButton.onPressed is delegated to InkWell.onTap, which then goes to a InkResponse.onTap, which then calls it on the last line of:

  void _handleTap(BuildContext context) {
    _currentSplash?.confirm();
    _currentSplash = null;
    updateHighlight(_HighlightType.pressed, value: false);
    if (widget.onTap != null) {
      if (widget.enableFeedback)
        Feedback.forTap(context);
      widget.onTap();
    }
  }

This is wrapped by the GestureRecognizer in an invokeCallback that does a try-catch and converts the error to a FlutterError that you see formatted nicely on your console.

T invokeCallback<T>(String name, RecognizerCallback<T> callback, { String debugReport() }) {
    assert(callback != null);
    T result;
    try {
      assert(() {
        if (debugPrintRecognizerCallbacksTrace) {
          final String report = debugReport != null ? debugReport() : null;
          // The 19 in the line below is the width of the prefix used by
          // _debugLogDiagnostic in arena.dart.
          final String prefix = debugPrintGestureArenaDiagnostics ? ' ' * 19 + '❙ ' : '';
          debugPrint('$prefix$this calling $name callback.${ report?.isNotEmpty == true ? " $report" : "" }');
        }
        return true;
      }());
      result = callback();
    } catch (exception, stack) {
      InformationCollector collector;
      assert(() {
        collector = () sync* {
          yield StringProperty('Handler', name);
          yield DiagnosticsProperty<GestureRecognizer>('Recognizer', this, style: DiagnosticsTreeStyle.errorProperty);
        };
        return true;
      }());
      FlutterError.reportError(FlutterErrorDetails(
        exception: exception,
        stack: stack,
        library: 'gesture',
        context: ErrorDescription('while handling a gesture'),
        informationCollector: collector
      ));
    }
    return result;
  }

The TextField.onSubmitted on the other hand is passed to a EditableText.onSubmitted and it's called on the last line of:

void _finalizeEditing(bool shouldUnfocus) {
    // Take any actions necessary now that the user has completed editing.
    if (widget.onEditingComplete != null) {
      widget.onEditingComplete();
    } else {
      // Default behavior if the developer did not provide an
      // onEditingComplete callback: Finalize editing and remove focus.
      widget.controller.clearComposing();
      if (shouldUnfocus)
        widget.focusNode.unfocus();
    }

    // Invoke optional callback with the user's submitted content.
    if (widget.onSubmitted != null)
      widget.onSubmitted(_value.text);
  }

This is wrapped by a MethodChannel:

Future<ByteData> _handleAsMethodCall(ByteData message, Future<dynamic> handler(MethodCall call)) async {
    final MethodCall call = codec.decodeMethodCall(message);
    try {
      return codec.encodeSuccessEnvelope(await handler(call));
    } on PlatformException catch (e) {
      return codec.encodeErrorEnvelope(
        code: e.code,
        message: e.message,
        details: e.details,
      );
    } on MissingPluginException {
      return null;
    } catch (e) {
      return codec.encodeErrorEnvelope(code: 'error', message: e.toString(), details: null);
    }
  }

And that last catch (e) is activated and then you don't see an error.

I suspect if you compile your App to Release you will see it, as the Dart VM has a problem in delivering exceptions in Debug mode for asynchronous code (I am over simplifying things here).

Share:
1,551
Patrick
Author by

Patrick

Updated on December 24, 2022

Comments

  • Patrick
    Patrick over 1 year

    I'm trying to debug a Flutter app. I noticed that in some cases, exceptions are thrown, but not displayed in the console. So it took me a while before knowing there was an exception. This is a lot of time wasted.

    Here is a small code snippet to show the problem. With RaisedButton the exception is shown, but not with TextField. I'm forced to add a try/catch to print the exception, or else it is invisible.

    The problem is not the error itself, the problem is that the error is not shown. Please tell me how I can show all exceptions.

    void main() => runApp(Test());
    
    class Test extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          home: Scaffold(
            body: Center(
              // EXCEPTION CAUGHT:
              // child: RaisedButton(child: Text('Test'), onPressed: () => throw Exception()),
              // EXCEPTION NOT CAUGHT:
              child: TextField(onSubmitted: (value) => throw Exception()),
            ),
          ),
        );
      }
    }
    
    • Abion47
      Abion47 over 3 years
      What is the error message? Also, Flutter tends to sometimes be overly verbose and somewhat cryptic with its error messages, so you might need to scroll up in the console window a ways in order to see the error.
    • Patrick
      Patrick over 3 years
      The problem is not the error itself. The problem is that the error is not shown.
    • Patrick
      Patrick over 3 years
      It can be ANY error. The TextField hides them.
  • Patrick
    Patrick over 3 years
    Thanks for your hunt. I also found this bug report but I don't know if it's related or not: github.com/flutter/flutter/issues/52420
  • Abion47
    Abion47 over 3 years
    @Patrick If the underlying issue is that the MethodChannel is swallowing errors from handlers of platform-sourced actions, then it probably is a related issue.
  • Abion47
    Abion47 over 3 years
    Running the app in release mode still does not show the error in the device logs. I'm pretty sure the root cause has nothing to do with asynchronicity.
  • Michel Feinstein
    Michel Feinstein over 3 years
    Than you should open this as an issue in the main repo codec.encodeErrorEnvelope is probably not being handled well.
  • Abion47
    Abion47 over 3 years
    I have. I've linked to the issue page in my answer.