Skip to content

JavaScript Engine Differences: iOS vs Android

Airlock runs rule scripts on two different JavaScript engines depending on the platform. The API surface — event, sharedState, serializeEvent(), buildProductString(), return value semantics — is identical on both. The engines that execute your code are not.

Understanding the differences matters when a script behaves differently on iOS and Android, when you hit a runaway-script termination, or when a subtle language feature causes a silent failure on one platform.


The two engines

iOS Android
Engine JavaScriptCore (JSC) Mozilla Rhino 1.9.1
Origin Built into the OS (WebKit) JVM-based interpreter, bundled in the Airlock AAR
Execution model JIT-compiled Interpreted
Runaway protection 2-second wall-clock timeout 100,000-instruction limit
Host (Java/ObjC) access Blocked by JSC API isolation Blocked by ClassShutter { false }
Prototype mutation All standard prototypes frozen Date.prototype and RegExp.prototype not frozen
Timer globals setTimeout / setInterval / setImmediate set to undefined Not injected; accessing them returns undefined

Language support

Safe on both platforms

Write scripts against this baseline and they will behave identically on iOS and Android:

Feature Notes
ES5 syntax var, function, prototype, all ES5 array and string methods
let / const Block scoping works on both
Arrow functions (x) => x * 2 works on both
Template literals `Hello ${name}` works on both
Destructuring Object and array destructuring work on both
Default parameters function f(x = 0) works on both
Rest / spread ...args and { ...obj } work on both
for...of Works over arrays on both; avoid iterating custom iterables
Map / Set Available on both
Symbol Available on both
Short-circuit assignment (??, &&=, \|\|=) Works on both
Optional chaining (?.) Works on both
Nullish coalescing (??) Works on both
JSON.parse / JSON.stringify Works on both
Object.keys / Object.assign / Object.entries Works on both
Regular expressions Core regex syntax works on both; see Regex caveats

Avoid on both platforms

These features are blocked or unsupported on both engines:

Feature Why
async / await / Promise Airlock scripts must be synchronous; async constructs either error or stall the 2-second timeout
ES modules (import / export) Not supported in either embedded context
eval / Function() constructor Blocked by Airlock's sandbox on both platforms
DOM / BOM APIs No browser context exists
fetch / XMLHttpRequest No network access
setTimeout / setInterval Not available (see table above)
Prototype mutation (Object.defineProperty on built-ins) Blocked on iOS; silently succeeds on Android but affects all subsequent evaluations in the session

Android-specific limitations

Rhino 1.9.1 is an ES5 interpreter with ES6 extensions. A small set of newer syntax either fails silently or throws on Android but not iOS:

Feature iOS (JSC) Android (Rhino)
class syntax ✗ — throws SyntaxError
WeakMap / WeakSet ✗ — not available
WeakRef / FinalizationRegistry ✗ — not available
Numeric separators (1_000_000) ✗ — throws SyntaxError
Logical assignment (&&=, \|\|=, ??=) ✗ — throws SyntaxError on older Rhino builds
Array.at() / Object.hasOwn() ✗ — not available
Named capture groups in regex ((?<name>...)) ✗ — throws

Rule of thumb: if you only test on iOS (Simulator), your script may fail silently or throw on Android devices. Always test on a real Android device with Assurance before publishing.


Runaway protection

Both engines terminate a script that runs too long, suppress the event, and reset the JS context. The mechanisms differ.

iOS: wall-clock timeout (2 seconds)

JSC measures elapsed real time. A script that does expensive string work, large array iterations, or anything blocking for more than 2 seconds is terminated. This is predictable and consistent regardless of loop iteration count.

// Safe on iOS as long as it finishes in < 2 seconds
var result = '';
for (var i = 0; i < 10000; i++) {
    result += computeSomething(i); // will time out if slow
}

Android: instruction limit (100,000)

Rhino counts bytecode instructions. Simple operations (increments, comparisons, property lookups) each consume one or more instructions. A tight loop with simple arithmetic can hit 100,000 instructions far faster than 2 seconds of wall time.

// Can hit the Android instruction limit despite being nearly instant
var sum = 0;
for (var i = 0; i < 50000; i++) {
    sum += i; // ~3 instructions per iteration = ~150,000 total
}

Practical guidance:

  • Keep loops short. If you're iterating over a list, the list should have tens of items, not thousands.
  • Prefer built-in methods (Array.reduce, Array.map) over manual loops — built-in methods execute partially in native code on Rhino and consume fewer counted instructions.
  • If a script behaves correctly on iOS but the event is suppressed on Android with no error in airlock.error, the instruction limit is the likely cause.

Sandbox mechanisms

iOS (JavaScriptCore)

JSC runs the script inside an isolated JSContext. The Airlock host injects exactly three objects: event, sharedState, and the helper functions. Everything else — the Objective-C / Swift runtime, Foundation classes, file handles — is unreachable from inside the context by design. There is no way for a script to escape this boundary.

Timer globals (setTimeout, etc.) are explicitly set to undefined after context creation so that scripts attempting to use them get a clear failure rather than a silent no-op.

Android (Rhino)

Rhino runs in a JVM process where all Java classes are theoretically reachable. Airlock installs a ClassShutter that returns false for every class, which prevents Rhino from resolving any Java package or class reference. Accessing java.lang.Runtime or any other Java class throws a ReferenceError.

Date.prototype and RegExp.prototype cannot be frozen due to Rhino internals — Rhino mutates these prototypes during engine initialisation. Scripts should not attempt to modify these prototypes; the behaviour is undefined across engine restarts.


Regex caveats

Most regex syntax works identically on both platforms. Watch for these differences:

Regex feature iOS (JSC) Android (Rhino)
Named capture groups (?<name>...) ✗ — throws at parse time
Lookbehind assertions (?<=...) ✗ — throws at parse time
Unicode mode (/u flag) Partial — basic Unicode properties work; \p{...} does not
s (dotAll) flag ✗ — flag is silently ignored

Stick to ES5 regex syntax for cross-platform compatibility.


Writing cross-platform scripts

Follow these practices to write scripts that behave identically on iOS and Android:

1. Stay in ES5 + the safe ES6 subset. Arrow functions, let/const, template literals, destructuring, spread, optional chaining, and nullish coalescing are safe. class, numeric separators, and newer built-ins are not.

2. Keep loops small. Assume a limit of ~30,000 effective iterations on Android for simple loops. Use built-in array methods where possible.

3. Never modify built-in prototypes. Even though Android allows it, the mutation persists across evaluations within the same session and will affect subsequent rule executions in unpredictable ways.

4. Test on real Android hardware. The iOS Simulator runs JSC. Android emulators run Rhino. Syntax errors that JSC accepts silently will throw on Rhino. The only reliable cross-platform test is a real device connected to Assurance.

5. Check airlock.error when events are suppressed unexpectedly. A SyntaxError from Rhino failing to parse a modern JS construct will appear here. If the error only occurs on Android, it is almost certainly a language compatibility issue.


See also