Description
I noticed that the JavaScriptAndroid.runJavaScriptReturningResult method forwards the raw result from evaluateJavaScriptAsync directly to the Flutter callback without checking for empty-string outcomes. The Jetpack JavaScriptEngine silently coerces non-string JS expression results (numbers, booleans, objects, undefined) to an empty string "". Since this bridge is the primary way Flutter code receives JS evaluation results, empty-string coercion is invisible to the Dart side.
Evidence
1. Result passed directly via callback
In JavaScriptAndroid.kt L142-L149, the onSuccess callback forwards the result as-is:
Futures.addCallback(
js.evaluateJavaScriptAsync(javaScript), object : FutureCallback<String> {
override fun onSuccess(result: String) {
if (didSubmitResponse) {
Log.i("JavaScriptAndroid", "A response was received when didSubmitResponse is true: $result")
return
}
didSubmitResponse = true
callback(Result.success(result))
}
override fun onFailure(t: Throwable) {
cleanUpIfSandboxDead(t)
...
callback(Result.failure(t))
}
}, getMainExecutor()
)
When a JS expression evaluates to a non-string type, result will be "". This is passed to Result.success(""), indistinguishable from a script that genuinely returned an empty string.
2. Flutter/Dart callers have no way to distinguish success from silent coercion
Since the bridge communicates via Result<String?>, the Dart side sees a non-null, non-error string and treats it as valid output.
Why this may matter
- Dart code consuming the result cannot distinguish between a legitimate empty string and a silently coerced non-string JS value.
- Scripts written by Flutter developers that return numbers, objects, or booleans will silently produce
"" on Android, while potentially working differently on other platforms (e.g. iOS JavaScriptCore returns the stringified value).
- This creates a cross-platform behavioral inconsistency that is hard to debug.
Suggested hardening
- Add an explicit empty-string check in
onSuccess:
override fun onSuccess(result: String) {
if (didSubmitResponse) return
didSubmitResponse = true
if (result.isEmpty()) {
Log.w("JavaScriptAndroid", "evaluateJavaScriptAsync returned empty string - possible non-string JS result")
}
callback(Result.success(result))
}
- Document the behavior in the Flutter API so callers know to wrap non-string JS expressions with
String(...) or .toString().
- Optionally, wrap all user scripts on the Kotlin side with a
String(...) coercion to make behavior consistent.
Description
I noticed that the
JavaScriptAndroid.runJavaScriptReturningResultmethod forwards the raw result fromevaluateJavaScriptAsyncdirectly to the Flutter callback without checking for empty-string outcomes. The JetpackJavaScriptEnginesilently coerces non-string JS expression results (numbers, booleans, objects,undefined) to an empty string"". Since this bridge is the primary way Flutter code receives JS evaluation results, empty-string coercion is invisible to the Dart side.Evidence
1. Result passed directly via callback
In
JavaScriptAndroid.ktL142-L149, theonSuccesscallback forwards the result as-is:When a JS expression evaluates to a non-string type,
resultwill be"". This is passed toResult.success(""), indistinguishable from a script that genuinely returned an empty string.2. Flutter/Dart callers have no way to distinguish success from silent coercion
Since the bridge communicates via
Result<String?>, the Dart side sees a non-null, non-error string and treats it as valid output.Why this may matter
""on Android, while potentially working differently on other platforms (e.g. iOS JavaScriptCore returns the stringified value).Suggested hardening
onSuccess:String(...)or.toString().String(...)coercion to make behavior consistent.