Python SDK
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.
- Windows
- Mac
- Emulator
- Cloud device
- Phone
- Python SDK
On this page
- The hierarchy of Python macro exceptions
- Which calls raise, and which return None
- Patterns for a long-running macro
- Treat None as a state
- Wrap device calls: retry the recoverable, exit on a device not connected error
- Log at the right level
- Exit with a non-zero code
- Read the details of an error
- Prove a host before you depend on it
- Troubleshooting
- Could not discover RPC port
- RPC Error (-32001): no device connected
- Timeout waiting for response from server after 30.0s
- The run shows as failed but the log looks fine
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, it writes this code for you.
The hierarchy of Python macro exceptions
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 readWhen 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.
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:
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.
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:
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")
raisestr(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:
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.
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 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.
Next steps
Related pages
Thanks. If something is wrong, tell us in Discord.
Questions? Ask in Discord