Automation API

Android device automation

Error Handling

Handle crashes, dialogs, network issues, and recovery

Robust error handling is critical for production automations. This guide covers common error scenarios and how to handle them gracefully.

System Interruptions

System dialogs can appear at any time. Check for and handle them before processing app screens.

Crash Dialogs

App crashes show a system dialog with options to close or report:

Crash dialog handling
TypeScript
// Detect crash dialog
function isCrashDialog(screen: AndroidNode): boolean {
return !!findNodesById(screen, "android:id/aerr_close")
.find(n => n.clickable);
}
// Handle crash dialog
async function handleCrashDialog(screen: AndroidNode): Promise<boolean> {
const closeButton = findNodesById(screen, "android:id/aerr_close")
.find(n => n.clickable);
if (closeButton) {
console.warn("Crash dialog detected, closing...");
await closeButton.performAction(agent.constants.ACTION_CLICK);
await sleep(2000);
return true;
}
return false;
}

Phone Dialogs

Incoming calls or phone-related dialogs can interrupt automation:

Phone dialog handling
TypeScript
const PHONE_PACKAGE = "com.android.phone";
const PHONE_DIALOG_IDS = [
"com.android.phone:id/floating_end_call_action_button",
"com.android.phone:id/declineButton",
"com.android.phone:id/dismiss_button",
];
function isPhoneDialog(screen: AndroidNode): boolean {
const allNodes = getAllNodes(screen);
// All nodes are from phone package
return allNodes.every(n => n.packageName === PHONE_PACKAGE);
}
async function handlePhoneDialog(screen: AndroidNode): Promise<boolean> {
const allNodes = getAllNodes(screen);
// Look for dismiss/decline button
const dismissButton = allNodes.find(n =>
PHONE_DIALOG_IDS.includes(n.viewId || "")
);
if (dismissButton) {
console.warn("Phone dialog detected, dismissing...");
if (dismissButton.actions.includes(agent.constants.ACTION_CLICK)) {
await dismissButton.performAction(agent.constants.ACTION_CLICK);
} else {
// Fallback to random tap within bounds
dismissButton.randomClick();
}
await sleep(2000);
return true;
}
return false;
}

Notification Shade

Dismiss expanded notifications that might be blocking the UI:

Notification shade handling
TypeScript
async function dismissNotificationShade(): Promise<boolean> {
// Get all screens (including notification shade)
const screens = await agent.actions.allScreensContent();
const allNodes = screens.flatMap(s => getAllNodes(s));
// Look for notification rows
const notificationRow = allNodes.find(n =>
n.viewId === "com.android.systemui:id/expandableNotificationRow" &&
n.actions.includes(agent.constants.ACTION_DISMISS)
);
if (notificationRow) {
console.log("Dismissing notification...");
await notificationRow.performAction(agent.constants.ACTION_DISMISS);
await sleep(500);
return true;
}
// Check if shade is open (swipe down to close)
const shadePanel = allNodes.find(n =>
n.viewId === "com.android.systemui:id/notification_panel"
);
if (shadePanel) {
console.log("Closing notification shade...");
await agent.actions.swipe(540, 1500, 540, 500, 300);
await sleep(500);
return true;
}
return false;
}

Network Connectivity

Network monitoring
TypeScript
let isNetworkAvailable = true;
let networkDownTime = 0;
let networkDownTimestamp: number | undefined;
// Set up network callback
agent.utils.setNetworkCallback((available) => {
isNetworkAvailable = available;
if (!available && networkDownTimestamp === undefined) {
networkDownTimestamp = Date.now();
console.warn("Network disconnected!");
} else if (available && networkDownTimestamp) {
networkDownTime += Date.now() - networkDownTimestamp;
networkDownTimestamp = undefined;
console.log("Network restored");
}
});
// Handle no internet screen
async function handleNoInternet(screen: AndroidNode): Promise<boolean> {
const allNodes = getAllNodes(screen);
const noInternetIndicator = allNodes.find(n =>
n.text?.toLowerCase()?.includes("no internet") ||
n.text?.toLowerCase()?.includes("you're offline") ||
n.text?.toLowerCase()?.includes("check your connection")
);
if (noInternetIndicator || !isNetworkAvailable) {
console.warn("No internet, waiting...");
// Wait for network to come back
for (let i = 0; i < 30; i++) {
if (isNetworkAvailable) {
// Try to refresh
const retryButton = allNodes.find(n =>
n.clickable &&
(n.text?.toLowerCase() === "retry" ||
n.text?.toLowerCase() === "try again")
);
if (retryButton) {
await retryButton.performAction(agent.constants.ACTION_CLICK);
}
await sleep(2000);
return true;
}
await sleep(2000);
}
// Network didn't come back
throw new Error("NETWORK_TIMEOUT");
}
return false;
}

Complete Error Handler

Put it all together in a system error handler:

System error handler
TypeScript
// Call this at the start of each main loop iteration
async function handleSystemErrors(screen: AndroidNode): Promise<boolean> {
// Check and dismiss notification shade first
if (await dismissNotificationShade()) {
return true; // Screen changed, re-check
}
// Check for crash dialog
if (await handleCrashDialog(screen)) {
return true;
}
// Check for phone dialog
if (await handlePhoneDialog(screen)) {
return true;
}
// Check for no internet
if (await handleNoInternet(screen)) {
return true;
}
return false; // No system errors found
}
// Usage in main loop
async function mainLoop() {
while (maxSteps-- > 0) {
const screen = await agent.actions.screenContent();
// Handle system errors first
if (await handleSystemErrors(screen)) {
continue; // Re-check after handling
}
// Get screen state and process...
const state = detectScreenState(screen);
await handleScreenState(state, screen);
}
}

Retry Strategies

Simple Retry

Basic retry
TypeScript
async function withRetry<T>(
fn: () => Promise<T>,
maxAttempts: number = 3
): Promise<T> {
let lastError: Error | undefined;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error as Error;
console.warn(`Attempt ${attempt} failed: ${lastError.message}`);
if (attempt < maxAttempts) {
await sleep(1000 * attempt); // Increasing delay
}
}
}
throw lastError;
}

Conditional Retry

Conditional retry
TypeScript
async function retryOnCondition<T>(
fn: () => Promise<T>,
shouldRetry: (error: Error) => boolean,
maxAttempts: number = 3
): Promise<T> {
let lastError: Error | undefined;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error as Error;
if (!shouldRetry(lastError) || attempt >= maxAttempts) {
throw lastError;
}
console.warn(`Retrying after: ${lastError.message}`);
await sleep(2000);
}
}
throw lastError;
}
// Usage: Only retry on network errors
await retryOnCondition(
() => performNetworkOperation(),
(error) => error.message.includes("network") ||
error.message.includes("timeout"),
5
);

Recovery Actions

Recovery strategies
TypeScript
async function attemptRecovery(
state: ScreenState,
screen: AndroidNode
): Promise<boolean> {
console.log(`Attempting recovery from ${state}`);
switch (state) {
case ScreenState.Unknown:
// Try going back
await agent.actions.goBack();
await sleep(1000);
return true;
case ScreenState.Loading:
// Wait longer for loading
await sleep(5000);
return true;
case ScreenState.NoInternet:
// Toggle airplane mode to refresh connection
await agent.actions.airplane();
await sleep(5000);
return true;
case ScreenState.RateLimited:
// Wait before retrying
console.log("Rate limited, waiting 60 seconds...");
await sleep(60000);
return true;
case ScreenState.ErrorDialog:
// Try to dismiss error
const okButton = getAllNodes(screen).find(n =>
n.clickable &&
(n.text?.toLowerCase() === "ok" ||
n.text?.toLowerCase() === "dismiss")
);
if (okButton) {
await okButton.performAction(agent.constants.ACTION_CLICK);
await sleep(1000);
return true;
}
break;
}
return false;
}

Global Error Handler

Wrapping the automation
TypeScript
async function runAutomation() {
const files: File[] = [];
try {
// Set up callbacks
agent.utils.setNetworkCallback(handleNetworkChange);
agent.notifications.setNotificationCallback(handleNotification);
// Run main automation
await main();
// Success
await agent.utils.job.submitTask("success", await collectFinalData(), true, files);
} catch (error) {
console.error("Automation error:", error);
// Capture error screenshot
try {
const screenshot = await agent.actions.screenshot(1080, 1920, 80);
files.push({
name: "error_screenshot",
extension: "jpg",
base64Data: screenshot.base64
});
} catch (e) {
console.error("Failed to capture screenshot:", e);
}
// Determine failure type
const errorMessage = String(error);
let failureReason = "UNKNOWN_ERROR";
if (errorMessage.includes("OUT_OF_STEPS")) {
failureReason = "OUT_OF_STEPS";
} else if (errorMessage.includes("NETWORK")) {
failureReason = "NETWORK_ERROR";
} else if (errorMessage.includes("not found")) {
failureReason = "ELEMENT_NOT_FOUND";
}
// Submit failure
await agent.utils.job.submitTask("failed", {
error: errorMessage,
reason: failureReason,
stage: currentStage,
networkDownTime
}, true, files);
} finally {
// Clean up callbacks
agent.utils.setNetworkCallback(null);
agent.notifications.setNotificationCallback(null);
// Always stop the automation
stopCurrentAutomation();
}
}
// Start automation
runAutomation();

Best Practices

Always Check System States First

Before processing any app screen, check for crashes, dialogs, and notification shade. These can appear at any time and block automation.

Set Up Network Callback Early

Register the network callback at the start of your automation. Track total downtime for reporting.

Capture Screenshots on Failure

Always try to capture a screenshot when an error occurs. This is invaluable for debugging what went wrong.

Use Meaningful Error Reasons

Categorize errors into meaningful reasons (OUT_OF_STEPS, NETWORK_ERROR, etc.). This makes it easier to analyze failure patterns.

Clean Up Resources

Use try/finally to clean up callbacks and resources. This prevents memory leaks and unexpected behavior.

Next Steps