Every plugin call ends in cordova.exec(success, fail, service, action, args). It stores the callbacks under an ID (the service name plus a counter), turns args into JSON and calls _cordovaNative.exec(). Java calls the plugin's execute(action, args, callbackContext), the plugin answers with success() or error(), and the answer runs in the page as cordova.callbackFromNative(), which finds your function by its ID. A result sent with setKeepCallback(true) keeps the ID alive, so one call can report many times, as the battery-status plugin's listener does.
exec() predates Promises, so wrap it once. Run in the WebView's console (Remote Debugging), this wrapper calls the device plugin's service, then one that does not exist:
const execAsync = (service, action, args = []) =>
new Promise((resolve, reject) => cordova.exec(resolve, reject, service, action, args));
const info = await execAsync('Device', 'getDeviceInfo');
const missing = await execAsync('NoSuchPlugin', 'go').catch((err) => 'rejected: ' + err);
({ platform: info.platform, version: info.version, model: info.model, missing });{
"platform": "Android",
"version": "16",
"model": "sdk_gphone64_x86_64",
"missing": "rejected: Class not found"
}An unknown service reaches fail with Class not found, so a typo shows up as a rejection, not silence. The bridge also guards itself: at startup cordova.js receives a random bridgeSecret that every call must present. When the console called _cordovaNative.exec() with a guessed number, the call threw and logcat printed Bridge access attempt with wrong secret token, possibly from malicious code. Disabling exec() bridge!; after that, every cordova.exec() in the page failed until the app restarted.