Cross-language bridge error handling: JS-to-Java Example

All languages have certain semantics for dealing with error cases. C deals with them by setting error codes. Java deals with them by throwing exceptions. JavaScript deals with them by throwing exceptions as well but unlike Java, it does have any concept of checked Exceptions. The JS interpreter just stops. And this has some interesting implications in hybrid scenarios like a Webview based app.

Consider a simple Android app where most of the code is in JavaScript but is making a request to Java layer.

<html>
    <head/>
    <!-- Invoke getContacts() on the Javascript bridge object referenced via “JsInterface” tag on click -->
    <Button onClick='ContactJsInterface.getContacts()'>Click me</Button>
</html>
// MainActivity in Kotlin
class MainActivity : AppCompatActivity() {

  override fun onCreate(savedInstanceState: Bundle?) {
        ....
        // Load the above string into webview
        webView.loadDataWithBaseURL("about:blank", htmlPage, "text/html", null, null)
        webView.settings.javaScriptEnabled = true
        // Add a WebkitJsInterface object and tag it with "ContactJsInterface", so that,
        // JsInterface.getContacts maps to WebKitJsInterface.getContacts
        webView.addJavascriptInterface(WebkitJsInterface(this), "ContactJsInterface")
    }
}
// The Javascript interface object in Kotlin
class WebkitJsInterface(context: Context) {
    private val mContext = context

    // ContactJsInterface.getContacts() maps to this method
    @JavascriptInterface
    fun getContacts() {
        mContext.contentResolver.query(
            ContactsContract.Contacts.CONTENT_URI,
            null, null, null, null)
    }
}

 

After starting the app, check that the READ_CONTACTS is revoked or revoke it with adbe

adbe permissions revoke net.ashishb.jstojavademo contacts

Now, when you click the “Click me” button, you will see a SecurityException in the logs but the app won’t crash. If you check Thread’s name via Thread.currentThread().name, it will return JavaBridge. It seems any Exception thrown on this thread is simply swallowed. This won’t show up in analytics or crash log reports. Your Javascript code on return simply won’t be executed. And your app will appear unusable. This is worse than crashes. crashes at least give the app a chance to get out of a bad state.

This is worse than crashes. crashes at least give the app a chance to get out of a bad state.

Remedy

Sending error information across languages is hard. At the bare minimum, every such call should be encapsulated with a try-catch which catches Exception. For severe unexpected errors, it might not be bad to let the app crash, as you would, while writing the Java code.

@JavascriptInterface
fun getContacts() {
    try {
        getContactsUnsafe()
    } catch (e : Exception) {
        if (isSevereException(e)) {
            rethrowOnMainThread(e)
        } else {
            logError(e)
        }
    }
}

private fun logError(e: Exception) {
    Log.e("WebkitJsInterface", "Error occurred", e)
}

private fun rethrowOnMainThread(e: Exception) {
    Handler(Looper.getMainLooper()).post { throw e }
}

private fun getContactsUnsafe() {
    mContext.contentResolver.query(
        ContactsContract.Contacts.CONTENT_URI,
        null, null, null, null
    )
}

 

Full code for this blog post is posted on Github