Skip to content

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