# Error handling

Return the right errors to your callers and handle the errors external APIs return to you.

A REST service handles errors in two directions - it returns status codes and error messages to its own callers, and it reacts to failures of the external APIs it calls. This page shows both, with the [error catalog](https://zato.io/docs/dev/rest/error-catalog.html) as the reference for every status a channel can produce.

## Return errors to callers {#return-errors-to-callers}

When a service cannot fulfill a request, raise one of the exception classes from `zato.common.exception` - the channel maps the exception to its HTTP status code and the response body contains the message along with the request's CID:

```python
# -*- coding: utf-8 -*-

# Zato
from zato.common.exception import NotFound
from zato.server.service import Service

class GetCustomer(Service):
    """ Returns a customer by ID, with 404 for IDs that match nothing.
    """
    name = 'demo.rest.get-customer-by-id'

    input = 'customer_id'

    def handle(self) -> 'None':

        customer_id = self.request.input.customer_id
        customer = self.invoke('adapter.crm.customer.get', customer_id=customer_id)

        # No such customer means 404 for our caller
        if not customer:
            raise NotFound(self.cid, f'No such customer: {customer_id}')

        self.response.payload = customer
```

Each exception class maps to one status code - `BadRequest` to 400, `NotFound` to 404, `Conflict` to 409, `ServiceUnavailable` to 503 - the full table is in the [error catalog](https://zato.io/docs/dev/rest/error-catalog.html). Any other exception, Python built-ins included, is returned to the caller as 500.

An existing and a non-existent customer produce different responses:

```bash
# An existing customer - 200 with data
curl http://localhost:11223/api/customer/CUST-123

# A non-existent customer - 404 with the error message and CID
curl http://localhost:11223/api/customer/DOES-NOT-EXIST
```

When the response needs a status that no exception expresses - 201 Created for a new resource - or a structured error body of your own, set `self.response.status_code` and the payload yourself:

```python
# -*- coding: utf-8 -*-

# stdlib
from http import HTTPStatus

# Zato
from zato.server.service import Service

class CreateOrder(Service):
    name = 'demo.rest.create-order-endpoint'

    def handle(self) -> 'None':

        order = self.invoke('adapter.orders.create', data=self.request.payload)

        # 201 tells the caller a new resource exists and where it is
        self.response.status_code = HTTPStatus.CREATED
        self.response.headers['Location'] = f'/api/orders/{order["id"]}'
        self.response.payload = order
```

## Validation errors {#validation-errors}

A service with a [data model](https://zato.io/docs/dev/rest/channels.html) receives only requests that match the model - a request with a missing field or a wrong type is answered 400 before the service runs, and the service itself contains no validation code.

Business rules the model cannot express are checked in the service, which raises `BadRequest` with the reason:

```python
# -*- coding: utf-8 -*-

# stdlib
from dataclasses import dataclass

# Zato
from zato.common.exception import BadRequest
from zato.server.service import Model, Service

@dataclass(init=False)
class CreateOrderRequest(Model):
    customer_id: str
    sku: str
    quantity: int

class ValidateOrder(Service):
    name = 'demo.rest.validate-order'

    input = CreateOrderRequest

    def handle(self) -> 'None':

        request = self.request.input

        # The model has already guaranteed that quantity is an int -
        # the business rule that it must be positive is checked here
        if request.quantity < 1:
            raise BadRequest(self.cid, 'quantity must be positive')

        order = self.invoke('adapter.orders.create',
            customer_id=request.customer_id,
            sku=request.sku,
            quantity=request.quantity
        )

        self.response.payload = order
```

## Handle errors from external APIs {#handle-errors-from-external-apis}

An external API's status code arrives in the response object - decide per code what your own caller receives:

```python
# -*- coding: utf-8 -*-

# stdlib
from http import HTTPStatus

# Zato
from zato.common.exception import NotFound
from zato.server.service import Service

class CallExternalAPI(Service):
    name = 'demo.rest.call-external-api'

    def handle(self) -> 'None':

        conn = self.rest['Customer API']
        response = conn.get()

        # Success passes the data through ..
        if response.status_code == HTTPStatus.OK:
            self.response.payload = response.data

        # .. the external 404 becomes our own 404 ..
        elif response.status_code == HTTPStatus.NOT_FOUND:
            raise NotFound(self.cid, 'Resource not found in the external system')

        # .. and everything else is a gateway problem the caller cannot fix,
        # so the details go to the log and the caller receives 502.
        else:
            self.logger.error('External API error: %s - %s', response.status_code, response.data)
            self.response.status_code = HTTPStatus.BAD_GATEWAY
            self.response.payload = {'error': 'External service error'}
```

A connection error or a timeout raises an exception inside the service. Catch it when the caller should receive 503 rather than 500:

```python
# -*- coding: utf-8 -*-

# stdlib
from traceback import format_exc

# Zato
from zato.common.exception import ServiceUnavailable
from zato.server.service import Service

class SafeAPICall(Service):
    name = 'demo.rest.safe-api-call'

    def handle(self) -> 'None':

        try:
            conn = self.rest['Unreliable API']
            response = conn.get()
        except Exception:
            self.logger.error('API call failed, e:`%s`', format_exc())
            raise ServiceUnavailable(self.cid, 'The upstream API is unavailable')

        self.response.payload = response.data
```

## Partial failures in orchestration {#partial-failures-in-orchestration}

A service that calls several APIs can determine for each source whether a failure ends the request or only removes one part of the response:

```python
# -*- coding: utf-8 -*-

# stdlib
from traceback import format_exc

# Zato
from zato.common.exception import ServiceUnavailable
from zato.server.service import Service

class GetDashboard(Service):
    """ Combines profile, notifications and activity into one response.
    """
    name = 'demo.rest.get-dashboard'

    input = 'user_id'

    def handle(self) -> 'None':

        user_id = self.request.input.user_id
        result = {'user_id': user_id, 'errors': []}

        # The profile is required - without it there is nothing to return
        try:
            result['profile'] = self.invoke('adapter.users.get', user_id=user_id)
        except Exception:
            self.logger.error('User profile failed, e:`%s`', format_exc())
            raise ServiceUnavailable(self.cid, 'User profile is unavailable')

        # Notifications are optional - the response names what is missing
        try:
            result['notifications'] = self.invoke('adapter.notifications.list', user_id=user_id)
        except Exception:
            self.logger.warning('Notifications failed, e:`%s`', format_exc())
            result['notifications'] = []
            result['errors'].append('notifications_unavailable')

        # Activity is optional too
        try:
            result['activity'] = self.invoke('adapter.activity.recent', user_id=user_id)
        except Exception:
            self.logger.warning('Activity failed, e:`%s`', format_exc())
            result['activity'] = []
            result['errors'].append('activity_unavailable')

        self.response.payload = result
```

To learn more about combining APIs, see [orchestration](https://zato.io/docs/dev/rest/orchestration.html).

## Timeouts and retries {#timeouts-and-retries}

Timeouts and retries are configured on the [outgoing connection](https://zato.io/docs/dev/rest/outconns.html), so service code contains no retry logic. A connection retries timeouts, connection errors and HTTP 429 responses with backoff, honours the `Retry-After` header and gives up after its configured budget - see [Delivery retries and dead-letter queues](https://zato.io/docs/dev/delivery.html) and [timeouts](https://zato.io/docs/dev/rest/outconns.html#timeouts).

## Log errors {#log-errors}

Log expected errors as warnings and unexpected ones as errors, so the log separates input problems from failures that need attention:

```python
# -*- coding: utf-8 -*-

# stdlib
from traceback import format_exc

# Zato
from zato.common.exception import BadRequest
from zato.server.service import Service

class LoggingExample(Service):
    name = 'demo.rest.logging-example'

    def handle(self) -> 'None':

        try:
            result = self.process_request()
            self.response.payload = result

        except ValueError as e:

            # An input problem - the caller receives the reason as 400
            self.logger.warning('Validation error, e:`%s`', e)
            raise BadRequest(self.cid, str(e))

        except Exception:

            # An unexpected failure - the traceback goes to the log
            # and the caller receives a 500
            self.logger.error('Unexpected error, e:`%s`', format_exc())
            raise

    def process_request(self) -> 'dict':
        return {'status': 'ok'}
```

Every error body a channel produces includes the request's CID, so a caller reporting an error gives you the exact log entries to read.

## See also {#see-also}

- [Error catalog](https://zato.io/docs/dev/rest/error-catalog.html) - Every status code a channel returns and what causes each
- [Calling REST APIs](https://zato.io/docs/dev/rest/calling-apis.html) - Response objects, status checks and non-JSON responses
- [Delivery retries and dead-letter queues](https://zato.io/docs/dev/delivery.html) - The retries, queues and dead-letter queues that decide when errors are reported
- [Orchestration](https://zato.io/docs/dev/rest/orchestration.html) - Failures across multiple APIs combined in one service

## Learn more {#learn-more}

- [REST integration overview](https://zato.io/integrations/rest/) - Start here to see how teams build REST integrations
- [In-depth API tutorial](https://zato.io/tutorials/main/01.html) - Master architectural patterns for production-ready integrations
- [Modern REST API tutorial](https://zato.io/tutorials/rest-api/python.html) - Design, build and secure REST APIs with Python
