Error Handling

This tutorial describes how to write a robust script by handling SOTI MobiControl-specific errors. It also teaches how to terminate a script by throwing pre-defined or custom errors. Finally, it explains how the Android Agent JavaScript engine improves reporting programming mistakes.

Writing a Robust Script

A robust script should take into account that the Android Agent JavaScript APIs may not be supported or have limited functionality on selected devices and agents. For example, APN APIs are not supported on Android Enterprise agents running without the plugin on non-Samsung devices; File APIs will fail when accessing files out of the scoped storage, when running on Android 11; etc. Depending on the approach used to report such errors, the APIs can be split into several categories:

Nullable APIs

Nullable APIs return null when error conditions occur.
These APIs are documented as nullable in the API reference. For example, check mobicontrol.app.installedApps or mobicontrol.io.File.contentEquals.

If a property is nullable or a function returns nullable, a robust script is expected to have a null check:

var installedApps = mobicontrol.app.installedApps;
if (installedApps == null) {
    mobicontrol.log.error("Cannot obtain the list of installed apps");
} else {
    var installedAppPackageNames = installedApps
        .map(app => app.packageName)
        .join('|');
    mobicontrol.log.info(installedAppPackageNames);
}

Note that if the null check is omitted, and mobicontrol.app.installedApps returns null, the script will fail with an error similar to TypeError: Cannot read property "map" from null. It is not trivial to figure out the root cause of the failure from such an error.

Throwing APIs

Throwing APIs will throw an error when error conditions occur.
These APIs are documented as throwing an error in the API reference. For example, mobicontrol.audio.setRingVolume is documented to throw SetVolumeError. In order to see all the possible error codes the API can throw, check the documentation for the statusCode property of the error. For setRingVolume, this will be SetVolumeStatusCode, which has two possible values: UNKNOWN and DO_NOT_DISTURB_MODE.

If a function is documented to throw an error, like mobicontrol.audio.setRingVolume, a script may want to handle a specific error code.
For example, the following code tries to increase the audio volume. If increasing the audio volume fails because the device is in do-not-disturb (DND) mode, then the script will try to exit DND mode and retry setting the volume. Exiting DND mode is implemented by injecting the volume up key several times, with a 200ms delay between injections.

try {
    adjustRingVolume();
} catch (err) {
    if (err.statusCode == mobicontrol.audio.SetVolumeStatusCode.DO_NOT_DISTURB_MODE) {
        exitDndModeAndAdjustRingVolume();
    } else {
        throw err;
    }
}

function adjustRingVolume() {
    mobicontrol.audio.setRingVolume(0.8);
    mobicontrol.log.info('Ring volume adjusted')
}

function exitDndModeAndAdjustRingVolume() {
    const KEYCODE_VOLUME_UP = 24;
    var delay = 0;
    for (var i = 0; i < 3; i++) {
        setTimeout(injectVolumeUp, delay);
        delay += 200;
    }
    setTimeout(adjustRingVolume, delay);
}

function injectVolumeUp() {
    const KEYCODE_VOLUME_UP = 24;
    mobicontrol.device.injectKey(KEYCODE_VOLUME_UP);
}

Note that if the error is not DO_NOT_DISTURB_MODE, it is re-thrown. It is usually recommended to re-throw errors which do not require special handling, as this will produce the most useful feedback to the SOTI MobiControl console administrator.

Callback APIs

Callback APIs report error conditions by setting specific properties of the callback function's result parameter. A general success status is reported by result.isSuccessful and result.isFailure, while details can be obtained via result.statusCode and result.isTimedOut.
For example, mobicontrol.app.install accepts a callback parameter of installCallback type. If the callback fails due to timeout, result.isFailed and result.isTimedOut will be true. If the callback fails for any other reason, result.statusCode will have the detailed status code of the callback.
In order to see all the possible status codes, check the documentation of the statusCode's type. For installCallback, this will be InstallationStatusCode, which has five possible values.

A robust script is expected to check for callback errors and report these to the SOTI MobiControl console either by explicit logging or by throwing errors.
The following script demonstrates how to properly handle error conditions in an app installation callback:

mobicontrol.app.install('/sdcard/example.apk', onFinish);

function onFinish(result) {
    if (result.isSuccessful) {
        if (result.statusCode == mobicontrol.app.InstallationStatusCode.ALREADY_INSTALLED) {
            mobicontrol.log.info('Installation skipped, as the app is already installed.')
        }
        mobicontrol.app.start('net.soti.example');
    } else {
        if (result.isTimedOut) {
            throw 'Installation timed out.'
        } else {
            throw result.statusCode
        }
    }
}

Note that unlike statusCode of the throwing APIs, callbacks might have successful status codes. In the example above, this is illustrated by InstallationStatusCode.ALREADY_INSTALLED.

Throwing Errors

A script can throw an error to terminate its execution, as shown in previous examples. No matter where throw is called from - a main body, a function or a callback - the execution of the entire script will be terminated, and an error message will be logged to the SOTI MobiControl console. The error message is calculated by calling toString on the thrown object, with debug information (file name and line number where the error was thrown) attached. For example, if the app installation script would fail with FILE_NOT_FOUND, the following error message is expected: FILE_NOT_FOUND (UserScript#13).

Throwable APIs

The errors thrown in the app installation script are custom errors. The Android Agent JavaScript defines several APIs which are designed to be thrown by a script to indicate certain termination conditions. For example, if Termination.ABORTED is thrown from a package pre-installation script, the package installation is aborted and shown as "Failed" in the SOTI MobiControl console. Throwable APIs do not display error logs on the console when thrown, unlike custom errors.

The following script aborts package installation if it is running on a pre-Oreo device:

if (mobicontrol.os.apiLevel < 26) {
    throw(mobicontrol.packages.Termination.ABORTED);
}

Programming Errors

Many Android Agent JavaScript APIs throw Error when a programming error occurs. This is when the corresponding API is used incorrectly. For example, when a function is called with fewer arguments than required, or when an argument is invalid. This is done to improve debugging of JavaScript code and detect programming mistakes sooner.

Programming errors are not designed to be caught, therefore, they are not documented in APIs that may throw them. It is still possible to catch programming errors, as demonstrated by the following script:

try {
    // Call a function with missing argument
    var dialog = mobicontrol.message.createInfoDialog();
}
catch(error) {
    mobicontrol.log.warn(error.name + ' in ' + error.fileName + ' at line ' + error.lineNumber);
}
mobicontrol.log.info('Resuming the execution');

The script will log MobiControlError in UserScript at line 3. Note that the thrown error has the same properties as a Mozilla flavour of a standard JavaScript Error and its name property is equal to "MobiControlError". Errors thrown by Throwing APIs inherit from Error, adding a statusCode property.