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:
// 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:
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:
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
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:
// 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
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
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
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
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.