# Python Macro Exceptions and Errors in the mas SDK

> Python macro exceptions in the mas package with JSON-RPC codes, which calls raise and which return None, and how a long-running macro recovers or exits.

Source: https://automationmacro.com/docs/sdk/errors (Python SDK, updated 2026-09-04)

Every exception the `mas` package raises in a Python macro descends from one base class, and most carry the JSON-RPC code the app answered with. This page shows the hierarchy of those Python macro exceptions, which calls raise and which return `None`, and how a macro that runs for hours should handle each case. It is for anyone whose script has stopped with a traceback, or who wants it never to. If you describe the task to [MAS Agent](/agent), it writes this code for you.

## The hierarchy of Python macro exceptions

```text
MASError
  RPCError                    .code, .message, .data
    ConnectionError           could not reach the app (code -1)
    ParseError                -32700
    InvalidRequestError       -32600
    MethodNotFoundError       -32601
    InvalidParamsError        -32602
    InternalError             -32603
    DeviceNotConnectedError   -32001
    DeviceNotFoundError       -32002
    CommandFailedError        -32003
    ImageNotFoundError        -32004
    TimeoutError              -32005
    AuthenticationError       -32010
    SubscriptionError         -32011
  ElementsUnavailable         the accessibility tree could not be read
```

When the app answers a request with an error, the SDK looks the code up and raises the matching subclass; a code with no subclass raises plain `RPCError`. Every `RPCError` has three attributes: `code`, `message` and `data`. `ElementsUnavailable` is different: it is raised inside the SDK, descends from `MASError` directly, and has no code.

| Exception | Code | When you see it |
|---|---|---|
| `ConnectionError` | `-1` | No RPC port could be found, the connection was refused, or connecting timed out. The app is not running, or the script was started without it |
| `DeviceNotConnectedError` | `-32001` | A device call ran with no device bound. The emulator closed, the cloud device stopped, or the script started without one |
| `DeviceNotFoundError` | `-32002` | The requested device does not exist or is unavailable |
| `CommandFailedError` | `-32003` | A tap, swipe, key press or screenshot failed on the device |
| `ImageNotFoundError` | `-32004` | The image ID is not in your Image Library, or the file could not be loaded |
| `TimeoutError` | `-32005` | A host-side operation exceeded its time budget |
| `AuthenticationError` | `-32010` | You are not logged in to Macro Automation Studio (MAS) |
| `SubscriptionError` | `-32011` | An active subscription could not be confirmed |
| `MethodNotFoundError` | `-32601` | This host does not provide the method; check `host_capabilities` |
| `InvalidParamsError` | `-32602` | A parameter was outside what the host accepts |
| `InternalError` | `-32603` | The app hit an internal error |
| `ElementsUnavailable` | none | The accessibility tree could not describe this screen |

`mas.ConnectionError` and `mas.TimeoutError` shadow the Python built-ins of the same name. Always qualify them with `mas.` in an `except` clause.

## Which calls raise, and which return None

The rule: `None` means "not found, and that is fine". An exception means something is wrong.

| Call | On a miss | Can raise |
|---|---|---|
| `find_object`, `find_any_object`, `find_object_retry`, `find_any_object_retry` | `None` | `ImageNotFoundError` when the ID is unknown; `TypeError` when the ID is neither `int` nor `ImageRef` |
| `find_objects` | `[]` | The same |
| `wait_for_object` | `None` on timeout | `ImageNotFoundError`; it does not raise `TimeoutError` |
| `find_element`, `scroll_to_element` | `None` | `ElementsUnavailable` |
| `find_elements` | `[]` | `ElementsUnavailable` |
| `find_element_retry` | `None` | `ElementsUnavailable`, only when the final attempt still could not read the tree |
| `read_element` | `""` | `ElementsUnavailable` |
| `read_text` | never; the text may be empty | `TimeoutError`, `CommandFailedError` |
| `click`, `swipe`, `input_text`, `key_press`, `zoom_in`, `zoom_out` | none | `DeviceNotConnectedError`, `CommandFailedError` |
| `save`, `retrieve`, `clear` | `retrieve` returns `{}` | `ValueError` when `data` is not JSON or `port` is not the current device's port |
| `webhook` | none | `RuntimeError` when the event could not be queued |
| `mas.ui.batch` | none | `RuntimeError` when nested |
| `mas.ui.cell_button` | none | `ValueError` for an unknown `variant` |
| Any call | none | `ConnectionError`, `AuthenticationError`, `SubscriptionError`, `InternalError` |

Three cases deserve a closer look.

**`find_object` does not raise on a miss.** A template that exists but is not on screen returns `None`. Only an ID that is missing from your library raises `ImageNotFoundError`, and that is a bug in the script or the library, not a state of the game. Fix the ID rather than catching the error.

**`ElementsUnavailable` is not "element absent".** A game drawing into a canvas, a secure screen, an animating screen where only the structural tree could be read, or a dump that failed all raise it. The element you wanted may well be there. Catch it and fall back to vision with `find_object_retry`, or try again later. Never treat it as "already done".

**`SubscriptionError` mid-run is usually a network problem.** The subscription is checked once when the script starts. If the error appears later, the app could not confirm the subscription with the server; it does not mean the subscription lapsed. Exit cleanly and run again; if it persists, see [Billing](/docs/billing).

## Patterns for a long-running macro

A macro that loops for hours meets every kind of failure eventually. These patterns keep it honest: it recovers from the recoverable, stops on the fatal, and reports either way.

### Treat None as a state

Branch on the result instead of catching an exception:

```python
import mas

images = mas.images({"claim": 101})

button = mas.find_object_retry(images.claim, total_tries=3, time_sleep=2.0)
if button is None:
    mas.log("Nothing to claim this cycle", level="debug")
else:
    mas.click(button.x, button.y, delay_ms=1000)
```

`find_object_retry` already retries with a flat pause, so a screen that takes a moment to settle does not need a loop of your own.

### Wrap device calls: retry the recoverable, exit on a device not connected error

Recoverable errors are worth a pause and another try: `CommandFailedError`, `TimeoutError`, `ElementsUnavailable`. Fatal errors will not improve on their own: `DeviceNotConnectedError`, `ConnectionError`, `AuthenticationError`, `SubscriptionError`, `ImageNotFoundError`. Log and exit on those.

```python
import sys
import time
import mas

images = mas.images({"claim": 101})

RECOVERABLE = (mas.CommandFailedError, mas.TimeoutError, mas.ElementsUnavailable)
FATAL = (mas.DeviceNotConnectedError, mas.ConnectionError, mas.AuthenticationError,
         mas.SubscriptionError, mas.ImageNotFoundError)


def claim_once() -> bool:
    button = mas.find_object_retry(images.claim, total_tries=3, time_sleep=2.0)
    if button is None:
        return False
    mas.click(button.x, button.y, delay_ms=1000)
    return True


def main() -> int:
    failures = 0
    for cycle in range(200):
        try:
            if claim_once():
                failures = 0
                mas.log(f"Cycle {cycle}: claimed")
            else:
                mas.log(f"Cycle {cycle}: nothing to claim", level="debug")
        except RECOVERABLE as e:
            failures += 1
            mas.log(f"Cycle {cycle}: {e}", level="warning")
            if failures >= 5:
                mas.log("Five failures in a row, giving up", level="error")
                return 1
            time.sleep(10)
            continue
        except FATAL as e:
            mas.log(f"Fatal: {e}", level="error")
            return 1
        time.sleep(30)
    return 0


if __name__ == "__main__":
    sys.exit(main())
```

The counter matters. One failed swipe is noise; five in a row means the app is in a state the macro does not understand, and a human should look.

### Log at the right level

`mas.log` writes a leveled line to the run console, so a long log can be scanned for warnings and errors. Use `"debug"` for the routine, `"warning"` for a recovered failure, `"error"` for the reason you are about to exit. Plain `print` also reaches the console, without a level.

### Exit with a non-zero code

MAS records the process exit code with the run. Zero marks the run completed; anything else marks it failed, and a webhook endpoint subscribed to `macro.failed` receives the event with the exit code in the payload. So when the macro cannot do its job, return `1` from `main` (or `raise SystemExit(1)`) instead of returning quietly. An uncaught exception also exits with a non-zero code and puts the traceback in the run log, which is the right outcome for a bug you did not anticipate. Do not wrap the whole script in a bare `except Exception` that swallows everything; you would turn every failure into a run that looks like a success.

### Read the details of an error

Every `RPCError` carries the code and the message from the app, and sometimes extra data:

```python
import mas

try:
    mas.open_app("com.example.game")
except mas.RPCError as e:
    mas.log(f"code={e.code} message={e.message} data={e.data}", level="error")
    raise
```

`str(e)` renders as `RPC Error (<code>): <message>`, which is what you see in the traceback.

### Prove a host before you depend on it

A method one host provides and another lacks fails mid-run with `MethodNotFoundError`. `mas.host_capabilities()` returns the method names the connected host dispatches, so an elements-based path can be chosen once at startup:

```python
import mas

USE_ELEMENTS = "dump_hierarchy" in mas.host_capabilities()
```

## Troubleshooting

### Could not discover RPC port

`ConnectionError` at the first call. The script was started outside MAS, or the app is not running. Run scripts from the Code Editor or a device card; the app passes the connection details in the environment. See [Troubleshooting](/docs/troubleshooting).

### RPC Error (-32001): no device connected

`DeviceNotConnectedError`. The emulator closed or its ADB connection dropped, the cloud device stopped, or the run started without a device. Check the device card in **Device Groups**, then see [ADB troubleshooting](/docs/adb-troubleshooting) for emulators.

### Timeout waiting for response from server after 30.0s

A plain `RPCError` from the read timeout in the SDK. A single call took longer than 30 seconds, most often a full-screen `read_text` or a hierarchy dump on a busy screen. Pass a `region` to OCR, and catch `ElementsUnavailable` around element calls; the SDK converts a stalled dump into that exception so a vision fallback can take over.

### The run shows as failed but the log looks fine

The script exited with a non-zero code. Look at the last lines of the run log for a `return 1` path or a traceback. If you intended success, return `0` from `main`.
