Flutter exception not shown in console
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).
Patrick
Updated on December 24, 2022Comments
-
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 over 3 yearsWhat 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 over 3 yearsThe problem is not the error itself. The problem is that the error is not shown.
-
Patrick over 3 yearsIt can be ANY error. The TextField hides them.
-
-
Patrick over 3 yearsThanks 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 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 over 3 yearsRunning 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 over 3 yearsThan you should open this as an issue in the main repo
codec.encodeErrorEnvelope
is probably not being handled well. -
Abion47 over 3 yearsI have. I've linked to the issue page in my answer.