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 dialogfunction isCrashDialog(screen: AndroidNode): boolean { return !!findNodesById(screen, "android:id/aerr_close") .find(n => n.clickable);}
// Handle crash dialogasync 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 callbackagent.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 screenasync 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 iterationasync 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 loopasync 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 errorsawait 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 automationrunAutomation();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.