Search

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
Intermediate Updated 7 min read
On this page
  1. The hierarchy of Python macro exceptions
  2. Which calls raise, and which return None
  3. Patterns for a long-running macro
  4. Treat None as a state
  5. Wrap device calls: retry the recoverable, exit on a device not connected error
  6. Log at the right level
  7. Exit with a non-zero code
  8. Read the details of an error
  9. Prove a host before you depend on it
  10. Troubleshooting
  11. Could not discover RPC port
  12. RPC Error (-32001): no device connected
  13. Timeout waiting for response from server after 30.0s
  14. 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

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.

ExceptionCodeWhen you see it
ConnectionError-1No 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-32001A device call ran with no device bound. The emulator closed, the cloud device stopped, or the script started without one
DeviceNotFoundError-32002The requested device does not exist or is unavailable
CommandFailedError-32003A tap, swipe, key press or screenshot failed on the device
ImageNotFoundError-32004The image ID is not in your Image Library, or the file could not be loaded
TimeoutError-32005A host-side operation exceeded its time budget
AuthenticationError-32010You are not logged in to Macro Automation Studio (MAS)
SubscriptionError-32011An active subscription could not be confirmed
MethodNotFoundError-32601This host does not provide the method; check host_capabilities
InvalidParamsError-32602A parameter was outside what the host accepts
InternalError-32603The app hit an internal error
ElementsUnavailablenoneThe 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.

CallOn a missCan raise
find_object, find_any_object, find_object_retry, find_any_object_retryNoneImageNotFoundError when the ID is unknown; TypeError when the ID is neither int nor ImageRef
find_objects[]The same
wait_for_objectNone on timeoutImageNotFoundError; it does not raise TimeoutError
find_element, scroll_to_elementNoneElementsUnavailable
find_elements[]ElementsUnavailable
find_element_retryNoneElementsUnavailable, only when the final attempt still could not read the tree
read_element""ElementsUnavailable
read_textnever; the text may be emptyTimeoutError, CommandFailedError
click, swipe, input_text, key_press, zoom_in, zoom_outnoneDeviceNotConnectedError, CommandFailedError
save, retrieve, clearretrieve returns {}ValueError when data is not JSON or port is not the current device’s port
webhooknoneRuntimeError when the event could not be queued
mas.ui.batchnoneRuntimeError when nested
mas.ui.cell_buttonnoneValueError for an unknown variant
Any callnoneConnectionError, 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:

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.

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

Was this page helpful?

Questions? Ask in Discord