|
| 1 | +--- |
| 2 | +id: error-handling |
| 3 | +title: Error handling |
| 4 | +description: The exceptions an Actor can raise and how to handle them |
| 5 | +--- |
| 6 | + |
| 7 | +import HandleCallErrorsSource from '!!raw-loader!roa-loader!./code/13_handle_call_errors.py'; |
| 8 | +import RetryTimedOutSource from '!!raw-loader!roa-loader!./code/13_retry_timed_out.py'; |
| 9 | +import ApiLink from '@theme/ApiLink'; |
| 10 | +import RunnableCodeBlock from '@site/src/components/RunnableCodeBlock'; |
| 11 | + |
| 12 | +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. |
| 13 | + |
| 14 | +## Errors from the Apify API |
| 15 | + |
| 16 | +Every SDK operation that talks to the Apify API can raise <ApiLink to="class/ApifyApiError">`ApifyApiError`</ApiLink>. Such operations include <ApiLink to="class/Actor#start">`Actor.start`</ApiLink>, <ApiLink to="class/Actor#call">`Actor.call`</ApiLink>, <ApiLink to="class/Actor#abort">`Actor.abort`</ApiLink>, <ApiLink to="class/Actor#metamorph">`Actor.metamorph`</ApiLink>, <ApiLink to="class/Actor#add_webhook">`Actor.add_webhook`</ApiLink>, 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. |
| 17 | + |
| 18 | +<ApiLink to="class/ApifyApiError">`ApifyApiError`</ApiLink> dispatches to a subclass based on the HTTP status code: |
| 19 | + |
| 20 | +- <ApiLink to="class/UnauthorizedError">`UnauthorizedError`</ApiLink> (401) and <ApiLink to="class/ForbiddenError">`ForbiddenError`</ApiLink> (403) for an unauthorized or forbidden request. |
| 21 | +- <ApiLink to="class/NotFoundError">`NotFoundError`</ApiLink> (404) when the Actor, run, or storage doesn't exist. |
| 22 | +- <ApiLink to="class/ConflictError">`ConflictError`</ApiLink> (409) for a conflicting request. |
| 23 | +- <ApiLink to="class/RateLimitError">`RateLimitError`</ApiLink> (429) when the API rate limit is hit. |
| 24 | +- <ApiLink to="class/ServerError">`ServerError`</ApiLink> for any 5xx response. |
| 25 | +- <ApiLink to="class/InvalidRequestError">`InvalidRequestError`</ApiLink> (400) when the API rejects the request as malformed. |
| 26 | + |
| 27 | +The client retries rate-limited and server errors on its own, so you only see <ApiLink to="class/RateLimitError">`RateLimitError`</ApiLink> or <ApiLink to="class/ServerError">`ServerError`</ApiLink> once those retries are exhausted. The `apify.errors` module re-exports the whole client error hierarchy, so you can import everything from one place: |
| 28 | + |
| 29 | +```python |
| 30 | +from apify.errors import ApifyApiError, NotFoundError, RateLimitError |
| 31 | +``` |
| 32 | + |
| 33 | +To handle any API failure in one place, catch <ApiLink to="class/ApifyApiError">`ApifyApiError`</ApiLink>, then branch on the subclass or the HTTP `status_code`. To react to a specific failure, catch its subclass first: |
| 34 | + |
| 35 | +<RunnableCodeBlock className="language-python" language="python"> |
| 36 | + {HandleCallErrorsSource} |
| 37 | +</RunnableCodeBlock> |
| 38 | + |
| 39 | +## Misuse and invalid input |
| 40 | + |
| 41 | +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. |
| 42 | + |
| 43 | +- [`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. |
| 44 | +- [`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. |
| 45 | +- [`TypeError`](https://docs.python.org/3/library/exceptions.html#TypeError) for an argument of the wrong type. |
| 46 | +- [`ConnectionError`](https://docs.python.org/3/library/exceptions.html#ConnectionError) when <ApiLink to="class/Actor#create_proxy_configuration">`Actor.create_proxy_configuration`</ApiLink> verifies Apify Proxy access and the proxy reports that you have none. |
| 47 | + |
| 48 | +## Run failures |
| 49 | + |
| 50 | +<ApiLink to="class/Actor#call">`Actor.call`</ApiLink> and <ApiLink to="class/Actor#call_task">`Actor.call_task`</ApiLink> 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: |
| 51 | + |
| 52 | +<RunnableCodeBlock className="language-python" language="python"> |
| 53 | + {RetryTimedOutSource} |
| 54 | +</RunnableCodeBlock> |
| 55 | + |
| 56 | +## The pay-per-event charge limit |
| 57 | + |
| 58 | +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 <ApiLink to="class/Actor#charge">`Actor.charge`</ApiLink> call crosses the limit, only the part that fits within the budget is billed, and `charged_count` on the returned <ApiLink to="class/ChargeResult">`ChargeResult`</ApiLink> reports how many events went through. <ApiLink to="class/Actor#push_data">`Actor.push_data`</ApiLink> behaves the same way when given a `charged_event_name`. It writes only the items that fit within the budget. |
| 59 | + |
| 60 | +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](./pay-per-event). |
| 61 | + |
| 62 | +## Errors while crawling |
| 63 | + |
| 64 | +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 <ApiLink to="class/ApifyApiError">`ApifyApiError`</ApiLink>. For how to handle those errors, see [Errors from the Apify API](#errors-from-the-apify-api). |
| 65 | + |
| 66 | +## Conclusion |
| 67 | + |
| 68 | +Most failures you handle at runtime are <ApiLink to="class/ApifyApiError">`ApifyApiError`</ApiLink> 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 <ApiLink to="class/Actor#call">`Actor.call`</ApiLink>, check `run.status` to react to a failed run, and let Crawlee handle the errors raised inside a crawler. |
0 commit comments