# Error handling

Copy for LLM

When you run an Actor, exceptions come from a few layers: the Apify API client for failed API requests, the Apify SDK for misuse and invalid input, and the libraries you build on, such as Crawlee.

## Errors from the Apify API[](#errors-from-the-apify-api)

Every SDK operation that talks to the Apify API can raise [`ApifyApiError`](https://docs.apify.com/sdk/python/sdk/python/reference/class/ApifyApiError.md). Such operations include [`Actor.start`](https://docs.apify.com/sdk/python/sdk/python/reference/class/Actor.md#start), [`Actor.call`](https://docs.apify.com/sdk/python/sdk/python/reference/class/Actor.md#call), [`Actor.abort`](https://docs.apify.com/sdk/python/sdk/python/reference/class/Actor.md#abort), [`Actor.metamorph`](https://docs.apify.com/sdk/python/sdk/python/reference/class/Actor.md#metamorph), [`Actor.add_webhook`](https://docs.apify.com/sdk/python/sdk/python/reference/class/Actor.md#add_webhook), charging, and all storage operations on datasets, key-value stores, and request queues. The SDK raises these client exceptions as-is, so you keep the HTTP status code, the error type, and the response data on the exception.

[`ApifyApiError`](https://docs.apify.com/sdk/python/sdk/python/reference/class/ApifyApiError.md) dispatches to a subclass based on the HTTP status code:

* [`UnauthorizedError`](https://docs.apify.com/sdk/python/sdk/python/reference/class/UnauthorizedError.md) (401) and [`ForbiddenError`](https://docs.apify.com/sdk/python/sdk/python/reference/class/ForbiddenError.md) (403) for an unauthorized or forbidden request.
* [`NotFoundError`](https://docs.apify.com/sdk/python/sdk/python/reference/class/NotFoundError.md) (404) when the Actor, run, or storage doesn't exist.
* [`ConflictError`](https://docs.apify.com/sdk/python/sdk/python/reference/class/ConflictError.md) (409) for a conflicting request.
* [`RateLimitError`](https://docs.apify.com/sdk/python/sdk/python/reference/class/RateLimitError.md) (429) when the API rate limit is hit.
* [`ServerError`](https://docs.apify.com/sdk/python/sdk/python/reference/class/ServerError.md) for any 5xx response.
* [`InvalidRequestError`](https://docs.apify.com/sdk/python/sdk/python/reference/class/InvalidRequestError.md) (400) when the API rejects the request as malformed.

The client retries rate-limited and server errors on its own, so you only see [`RateLimitError`](https://docs.apify.com/sdk/python/sdk/python/reference/class/RateLimitError.md) or [`ServerError`](https://docs.apify.com/sdk/python/sdk/python/reference/class/ServerError.md) once those retries are exhausted. The `apify.errors` module re-exports the whole client error hierarchy, so you can import everything from one place:

```
from apify.errors import ApifyApiError, NotFoundError, RateLimitError
```

To handle any API failure in one place, catch [`ApifyApiError`](https://docs.apify.com/sdk/python/sdk/python/reference/class/ApifyApiError.md), then branch on the subclass or the HTTP `status_code`. To react to a specific failure, catch its subclass first:

[Run on](https://console.apify.com/actors/HH9rhkFXiZbheuq1V?runConfig=eyJ1IjoiRWdQdHczb2VqNlRhRHQ1cW4iLCJ2IjoxfQ.eyJpbnB1dCI6IntcImNvZGVcIjpcImltcG9ydCBhc3luY2lvXFxuXFxuZnJvbSBhcGlmeSBpbXBvcnQgQWN0b3JcXG5mcm9tIGFwaWZ5LmVycm9ycyBpbXBvcnQgQXBpZnlBcGlFcnJvciwgTm90Rm91bmRFcnJvclxcblxcblxcbmFzeW5jIGRlZiBtYWluKCkgLT4gTm9uZTpcXG4gICAgYXN5bmMgd2l0aCBBY3RvcjpcXG4gICAgICAgIHRyeTpcXG4gICAgICAgICAgICBydW4gPSBhd2FpdCBBY3Rvci5jYWxsKCdhcGlmeS93ZWItc2NyYXBlcicsIHJ1bl9pbnB1dD17J3N0YXJ0VXJscyc6IFtdfSlcXG4gICAgICAgIGV4Y2VwdCBOb3RGb3VuZEVycm9yOlxcbiAgICAgICAgICAgICMgQ2F0Y2ggYSBzcGVjaWZpYyBzdWJjbGFzcyBmaXJzdC5cXG4gICAgICAgICAgICBBY3Rvci5sb2cuZXJyb3IoJ1RoZSBBY3RvciB0byBjYWxsIGRvZXMgbm90IGV4aXN0LicpXFxuICAgICAgICAgICAgcmV0dXJuXFxuICAgICAgICBleGNlcHQgQXBpZnlBcGlFcnJvciBhcyBleGM6XFxuICAgICAgICAgICAgIyBBbnkgb3RoZXIgQVBJIGZhaWx1cmUsIGUuZy4gYW4gaW52YWxpZCB0b2tlbiBvciBhIHNlcnZlciBlcnJvci5cXG4gICAgICAgICAgICBBY3Rvci5sb2cuZXJyb3IoZidDYWxsaW5nIHRoZSBBY3RvciBmYWlsZWQ6IHtleGN9IChIVFRQIHtleGMuc3RhdHVzX2NvZGV9KS4nKVxcbiAgICAgICAgICAgIHJldHVyblxcblxcbiAgICAgICAgIyBgQWN0b3IuY2FsbGAgcmV0dXJucyB0aGUgZmluaXNoZWQgcnVuIHdoYXRldmVyIGl0cyBzdGF0dXMsIHNvIGNoZWNrIGl0LlxcbiAgICAgICAgaWYgcnVuLnN0YXR1cyAhPSAnU1VDQ0VFREVEJzpcXG4gICAgICAgICAgICBBY3Rvci5sb2cuZXJyb3IoZidSdW4ge3J1bi5pZH0gZW5kZWQgd2l0aCBzdGF0dXMge3J1bi5zdGF0dXN9LicpXFxuICAgICAgICAgICAgcmV0dXJuXFxuXFxuICAgICAgICBBY3Rvci5sb2cuaW5mbyhmJ1J1biB7cnVuLmlkfSBmaW5pc2hlZCBzdWNjZXNzZnVsbHkuJylcXG5cXG5cXG5pZiBfX25hbWVfXyA9PSAnX19tYWluX18nOlxcbiAgICBhc3luY2lvLnJ1bihtYWluKCkpXFxuXCJ9Iiwib3B0aW9ucyI6eyJidWlsZCI6ImxhdGVzdCIsImNvbnRlbnRUeXBlIjoiYXBwbGljYXRpb24vanNvbjsgY2hhcnNldD11dGYtOCIsIm1lbW9yeSI6MTAyNCwidGltZW91dCI6MTgwfX0.ZfrhXtdUi-aXULR92hreC-nHcr8dvabBDpbulBmX9Zk\&asrc=run_on_apify)

```
import asyncio



from apify import Actor

from apify.errors import ApifyApiError, NotFoundError





async def main() -> None:

    async with Actor:

        try:

            run = await Actor.call('apify/web-scraper', run_input={'startUrls': []})

        except NotFoundError:

            # Catch a specific subclass first.

            Actor.log.error('The Actor to call does not exist.')

            return

        except ApifyApiError as exc:

            # Any other API failure, e.g. an invalid token or a server error.

            Actor.log.error(f'Calling the Actor failed: {exc} (HTTP {exc.status_code}).')

            return



        # `Actor.call` returns the finished run whatever its status, so check it.

        if run.status != 'SUCCEEDED':

            Actor.log.error(f'Run {run.id} ended with status {run.status}.')

            return



        Actor.log.info(f'Run {run.id} finished successfully.')





if __name__ == '__main__':

    asyncio.run(main())
```

## Misuse and invalid input[](#misuse-and-invalid-input)

The SDK raises standard Python exceptions when it's used incorrectly or given invalid input. These exceptions point to a bug or a bad argument in your code, so the fix is to correct the call rather than to catch the exception.

* [`RuntimeError`](https://docs.python.org/3/library/exceptions.html#RuntimeError) when an `Actor` method is used outside the `async with Actor:` block, either before initialization or after exit, or when the Actor is initialized twice.
* [`ValueError`](https://docs.python.org/3/library/exceptions.html#ValueError) for an invalid argument, such as a malformed `timeout`, an invalid proxy configuration, charging an automatically charged event by hand, or pushing data that is not JSON-serializable or is over the size limit.
* [`TypeError`](https://docs.python.org/3/library/exceptions.html#TypeError) for an argument of the wrong type.
* [`ConnectionError`](https://docs.python.org/3/library/exceptions.html#ConnectionError) when [`Actor.create_proxy_configuration`](https://docs.apify.com/sdk/python/sdk/python/reference/class/Actor.md#create_proxy_configuration) verifies Apify Proxy access and the proxy reports that you have none.

## Run failures[](#run-failures)

[`Actor.call`](https://docs.apify.com/sdk/python/sdk/python/reference/class/Actor.md#call) and [`Actor.call_task`](https://docs.apify.com/sdk/python/sdk/python/reference/class/Actor.md#call_task) wait for the run to finish and return it, whatever its final status. A finished run can be `SUCCEEDED`, `FAILED`, `ABORTED`, or `TIMED-OUT`, so check `run.status` before you rely on the run's output. A timed-out run is the one case where retrying can help, as long as you give it more time:

[Run on](https://console.apify.com/actors/HH9rhkFXiZbheuq1V?runConfig=eyJ1IjoiRWdQdHczb2VqNlRhRHQ1cW4iLCJ2IjoxfQ.eyJpbnB1dCI6IntcImNvZGVcIjpcImltcG9ydCBhc3luY2lvXFxuZnJvbSBkYXRldGltZSBpbXBvcnQgdGltZWRlbHRhXFxuXFxuZnJvbSBhcGlmeSBpbXBvcnQgQWN0b3JcXG5cXG5cXG5hc3luYyBkZWYgbWFpbigpIC0-IE5vbmU6XFxuICAgIGFzeW5jIHdpdGggQWN0b3I6XFxuICAgICAgICB0aW1lb3V0ID0gdGltZWRlbHRhKG1pbnV0ZXM9NSlcXG4gICAgICAgIG1heF9hdHRlbXB0cyA9IDNcXG5cXG4gICAgICAgIGZvciBhdHRlbXB0IGluIHJhbmdlKDEsIG1heF9hdHRlbXB0cyArIDEpOlxcbiAgICAgICAgICAgIHJ1biA9IGF3YWl0IEFjdG9yLmNhbGwoJ2FwaWZ5L3dlYi1zY3JhcGVyJywgdGltZW91dD10aW1lb3V0KVxcblxcbiAgICAgICAgICAgIGlmIHJ1bi5zdGF0dXMgIT0gJ1RJTUVELU9VVCcgb3IgYXR0ZW1wdCA9PSBtYXhfYXR0ZW1wdHM6XFxuICAgICAgICAgICAgICAgIEFjdG9yLmxvZy5pbmZvKGYnUnVuIHtydW4uaWR9IGVuZGVkIHdpdGggc3RhdHVzIHtydW4uc3RhdHVzfS4nKVxcbiAgICAgICAgICAgICAgICBicmVha1xcblxcbiAgICAgICAgICAgIHRpbWVvdXQgKj0gMlxcbiAgICAgICAgICAgIEFjdG9yLmxvZy53YXJuaW5nKGYnVGltZWQgb3V0LCByZXRyeWluZyB3aXRoIHRpbWVvdXQge3RpbWVvdXR9LicpXFxuXFxuXFxuaWYgX19uYW1lX18gPT0gJ19fbWFpbl9fJzpcXG4gICAgYXN5bmNpby5ydW4obWFpbigpKVxcblwifSIsIm9wdGlvbnMiOnsiYnVpbGQiOiJsYXRlc3QiLCJjb250ZW50VHlwZSI6ImFwcGxpY2F0aW9uL2pzb247IGNoYXJzZXQ9dXRmLTgiLCJtZW1vcnkiOjEwMjQsInRpbWVvdXQiOjE4MH19.69GFtTj8uuT620PI0x-4nvEVJ8Qa5Def0XlYAj71H20\&asrc=run_on_apify)

```
import asyncio

from datetime import timedelta



from apify import Actor





async def main() -> None:

    async with Actor:

        timeout = timedelta(minutes=5)

        max_attempts = 3



        for attempt in range(1, max_attempts + 1):

            run = await Actor.call('apify/web-scraper', timeout=timeout)



            if run.status != 'TIMED-OUT' or attempt == max_attempts:

                Actor.log.info(f'Run {run.id} ended with status {run.status}.')

                break



            timeout *= 2

            Actor.log.warning(f'Timed out, retrying with timeout {timeout}.')





if __name__ == '__main__':

    asyncio.run(main())
```

## The pay-per-event charge limit[](#the-pay-per-event-charge-limit)

Reaching the pay-per-event charge limit doesn't raise an error. Instead, the SDK caps charging and data pushing, while your Actor keeps running. When a single [`Actor.charge`](https://docs.apify.com/sdk/python/sdk/python/reference/class/Actor.md#charge) call crosses the limit, only the part that fits within the budget is billed, and `charged_count` on the returned [`ChargeResult`](https://docs.apify.com/sdk/python/sdk/python/reference/class/ChargeResult.md) reports how many events went through. [`Actor.push_data`](https://docs.apify.com/sdk/python/sdk/python/reference/class/Actor.md#push_data) behaves the same way when given a `charged_event_name`. It writes only the items that fit within the budget.

To detect the limit, check the `event_charge_limit_reached` field on the `ChargeResult`. It's a return value and not an exception, so you can read it in a tight charging loop and stop your work once the budget runs out. For details, see [Pay-per-event monetization](https://docs.apify.com/sdk/python/sdk/python/docs/concepts/pay-per-event.md).

## Errors while crawling[](#errors-while-crawling)

If your Actor runs a [Crawlee](https://crawlee.dev/python) crawler, failures inside request handlers surface as Crawlee exceptions. Crawlee handles the retries and session rotation around them, so a single failing request doesn't stop the crawl. API calls you make from inside a handler still raise [`ApifyApiError`](https://docs.apify.com/sdk/python/sdk/python/reference/class/ApifyApiError.md). For how to handle those errors, see [Errors from the Apify API](#errors-from-the-apify-api).

## Conclusion[](#conclusion)

Most failures you handle at runtime are [`ApifyApiError`](https://docs.apify.com/sdk/python/sdk/python/reference/class/ApifyApiError.md) from the API client. Catch it to cover any API failure, and reach for a subclass or the HTTP `status_code` when you need finer control. The standard [`RuntimeError`](https://docs.python.org/3/library/exceptions.html#RuntimeError), [`ValueError`](https://docs.python.org/3/library/exceptions.html#ValueError), and [`TypeError`](https://docs.python.org/3/library/exceptions.html#TypeError) signal a bug or bad input, so correct the call rather than catch them. After [`Actor.call`](https://docs.apify.com/sdk/python/sdk/python/reference/class/Actor.md#call), check `run.status` to react to a failed run, and let Crawlee handle the errors raised inside a crawler.
