Eximia docs
Error model
How errors propagate from a plugin → native runtime → bridge → JS, and what JS code sees.
Goals
- Stable codes: JS code branches on
error.code, not message text. - Retryability hint: every error declares whether retrying makes sense.
- Plugin namespacing: codes carry their origin (
camera/...,eximia/...) so they don't collide. - No leaking stack traces in release builds.
Wire format
The error envelope from bridge-contract.md:
{
"v": 1,
"id": "c5e6a4f3-...",
"kind": "err",
"error": {
"code": "camera/permission-denied",
"message": "User denied camera permission",
"retryable": false,
"debug": { "android": { "api": 33 } }
}
}
code, message, and retryable are MANDATORY. debug is optional and
populated only in debug builds (BuildConfig.DEBUG on Android, #if DEBUG
on iOS).
Code taxonomy
Codes use the form <scope>/<reason> where <scope> is one of:
| Scope | Owned by | When |
|---|---|---|
eximia/... | The runtime | Bridge/transport/lifecycle errors. |
<plugin-name>/... | The plugin | Plugin-defined errors. |
Reserved eximia/* codes
| Code | Meaning | retryable |
|---|---|---|
eximia/unsupported-version | v in request envelope is unknown | false |
eximia/unknown-plugin | Plugin lookup failed | false |
eximia/unknown-action | Plugin exists but doesn't know the action | false |
eximia/invalid-args | args failed plugin-side validation | false |
eximia/main-thread-blocked | Plugin held the main thread too long (debug only) | true |
eximia/internal | Bridge crashed; check debug for details | true |
eximia/timeout | Plugin call exceeded its declared timeout | true |
eximia/permission-required | Plugin requires a permission the user hasn't granted | false |
Plugins SHOULD prefer their own scoped codes over reusing eximia/* codes.
Plugin code conventions
Plugins SHOULD use short, kebab-case reasons:
camera/permission-deniedcamera/cancelled← user dismissed pickercamera/no-camera← device has no cameracamera/decode-failed← image decode failedbiometric/lockoutbiometric/no-enrolledgeolocation/timeoutdownload/no-networkdownload/disk-full
When in doubt, mimic the codes the corresponding Cordova plugin used so
JS code that already handles "FILE_NOT_FOUND" or similar continues to
work after migration.
How a plugin reports an error
Kotlin
class SLMCamera : SLMEximiaPlugin() {
override suspend fun execute(
action: String, args: JsonObject, cb: SLMEximiaCallback
) {
if (!hasCameraPermission()) {
cb.error("camera/permission-denied", "Camera permission denied")
return
}
// ...
}
}
If the plugin throws instead of calling cb.error, the runtime
catches it and maps it to:
{ "code": "<plugin>/uncaught", "message": "<exception message>", "retryable": false }
The exception's stack trace is included in debug.stack (debug builds only).
Swift
class SLMCamera: SLMEximiaPlugin {
override func execute(
action: String, args: [String: Any], cb: SLMEximiaCallback
) async {
guard hasCameraPermission() else {
cb.error(code: "camera/permission-denied",
message: "Camera permission denied")
return
}
// ...
}
}
throws from execute works equivalently; the runtime maps thrown
errors to <plugin>/uncaught the same way.
How JS sees the error
try {
const result = await SLMEximia.plugin("Camera").take({ quality: 80 });
} catch (e) {
if (e instanceof SLMEximiaError) {
switch (e.code) {
case "camera/permission-denied":
showSettingsLink();
break;
case "camera/cancelled":
// user just dismissed, no-op
break;
default:
if (e.retryable) retryLater();
else surfaceUnknownError(e);
}
} else {
throw e; // not an SLMEximiaError, let it propagate
}
}
SLMEximiaError is a class exported from @slm/eximia:
class SLMEximiaError extends Error {
code: string;
retryable: boolean;
debug?: object; // present only in debug builds
}
What about success with an error-like payload?
Some Cordova plugins call success(...) even when the operation
"failed" from the user's POV (e.g. camera cancelled). SLMEximia plugins
SHOULD always use error() for non-success outcomes, with codes like
<plugin>/cancelled and retryable: false.
The runtime does not enforce this — it's a convention. Plugins that ignore it still work, but their JS callers have to inspect the success payload for soft failures, which is the pattern SLMEximia is designed to eliminate.
Debug payloads
In debug builds, the runtime adds context to error.debug:
{
"platform": "android",
"eximiaVersion": "1.0.0",
"pluginVersion": "1.2.0",
"androidApi": 33,
"stack": "java.lang.SecurityException at ..."
}
In release builds, debug is absent. JS code MUST NOT depend on it.
Reporting and analytics
The runtime emits a structured log line for every error response, regardless of build flavour:
W/SLMEximia exec failure plugin=Camera action=take id=c5e6a4f3 code=camera/permission-denied retryable=false
A future plugin can subscribe to these and ship them to a crash reporter or analytics backend; the runtime doesn't ship with one in v1.0.