# DTS Function Error Handling

This document provides a comprehensive technical overview of error handling in DTS Functions. It explains error categories, how to declare and structure errors, the use of value resolvers for robust and predictable error management, best practices for declarative error handling, and mechanisms for clients to reliably determine response types.

---

## Error Categories

DTS Functions must handle two categories of errors:

1. **System Errors**
   - Predefined by the DTS framework for common scenarios.
   - Ensure consistency and reliability across all DTS Functions.
2. **User-Defined Errors**
   - Defined by developers for specific function logic.

---

## Declaring Errors in DTS Functions

**Requirement:**  
DTS Functions must declare all possible error types using the `errors` property.  
All error types must be of type `cti.a.p.err.v1.0`.

**Example:**
```yaml
DTSFunction:
  (cti.cti): cti.a.p.dts.func.v1.0
  ...
  properties:
    ...
    errors?:
      type: object
      description: >
        Allows declaration of multiple error types. Only one can be returned at a time.
      (cti.schema):
        - cti.a.p.err.v1.0
    ...
```

## Error Definition in DTS Function

**Key Requirement:** 
Function developers should be able to return errors on error conditions in declarative DTS functions.


### CTI Traits: `error`

#### Description

* Defines a set of conditions under which the function returns an error object instead of a normal return value.

* Declaratively handles errors within serverless functions.

* Allows specifying custom error **conditions** and **corresponding error responses** using **DTS expression**.

* Allows for a **predictable** and **structured error** handling in declarative execution path.

* It is exclusive to the `source` and `module` traits, and if they are used, then `error` trait is ignored.

* If the returned error object doesn't comply with the declared error types, system will return:

```yaml
cti.a.p.err.v1.0~a.p.dts.func.v1.0~a.p.unexpected_return_type.v1.0
```

---

#### Declaration

It's declared in [dts/types.raml](../types.raml?plain=1#L319)

```yaml
facets:
  cti-traits:
    error:
      description: >
        *Please note:* It applies only to the declarative code and is ignored for the imperative code (e.g. source/module trait).

        The `error` CTI Trait specifies the error object returned by the DTS function to the caller.
        The 'return' CTI Trait is ignored when error is returned. The error object should be one of the declared error types in the `errors` attribute of the DTS Function.

        The `error` field is mutually exclusive with `return`, `source` and `module`.

      type: array
      items:
        properties:
          when:
            type: string
            description: >
              The error condition. Supports DTS expression Language (DEL).
              You have access to all the resolvers including `$.context`, `$.params`.

          error:
            type: object | string
            (cti.schema): cti.a.p.err.v1.0
            description: >
              Allows defining the user-defined error object to be returned to the caller.
              End developers can use the following options to define the error object:
                1. use dts_expression (DEL) that results in error object of type `cti.a.p.err.v1.0`
                   e.g. `$(func(~a.p.err_handler.v1 @!$.context.err.obj))`
                2. construct error object of type `cti.a.p.err.v1.0`

              The error object should be one of the error types declared in the `errors` attribute of the DTS function.
```

---

#### Example

```yaml
error:
  - when: $cel(.context.upstream.status_code != 200) # assuming `context.upstream = $http[]()`
    error:
      id: $uuid()
      type: cti.a.p.err.v1.0~a.p.dts.func.v1.0~a.p.invalid_response.v1.0
      timestamp: $cel(string(.now))
      source: $.cti # function’s CTI ID
      action: GET /api/v2/tenants?tenantId=123
      message: $.context.upstream.body.message
      payload:
        code: $.context.upstream.body.code
        domain: $.context.upstream.body.domain
        message: $.context.upstream.body.message
        details: $.context.upstream.body.details
```

---

### CTI Trait: `error_stack`

#### Description

* Method to propagate the upstream services (e.g., HTTP Requests) errors.
* Errors can include an `upstream_error` field if they propagate from another function.
* Error stack is returned only for authorized users with debugging scope in its token.
* Function in the stack will throw unique error object and the nested error, if any, will be encapsulated by the caller function’s error object.

> All errors reported as part of the upstream invocation will be part of the `upstream_error` when called function results in an error.

---

#### Declaration

```yaml
facets:
  cti-traits:
    error_stack:
      type: boolean
      description: include `upstream_errors` to the function error
      default: false
```

---

#### Example

```yaml
"DTS function upstream HTTP error":
  id: f2350881-39cd-43e7-a5ce-52f9ff637f06
  type: cti.a.p.err.v1.0~a.p.dts.upstream_error.v1.0
  timestamp: "2024-05-05T11:45:00.123456789Z"
  message: "Unexpected upstream HTTP error while generating the function response"
  action: "cti.a.p.dts.func.v0~example_app.my_another_function.v1.0"
  request: "02b2b1a24-9ebd-46da-87a7-e57dead0213d"
  tenant: "79ecdfae-01f8-4175-a947-a87f376ad6f5"
  upstream_error:
    id: 1861ac57-5572-49a8-8470-34b0a3768188
    type: cti.a.p.err.v1.0~a.p.http_error.v1.0
    code: 400
    timestamp: "2024-05-05T11:45:00.123456789Z"
    message: "Unsupported 'alertId' parameter"
    action: "GET /api/alerts/v2/alert?id=123"
    request: "02b2b1a24-9ebd-46da-87a7-e57dead0213d"
    tenant: "79ecdfae-01f8-4175-a947-a87f376ad6f5"
```
---

**Runtime Evaluation Order**:

  1. Evaluate `when` conditions in the error trait.
  2. **Short-circuit evaluation**: First condition that evaluates to true is selected; corresponding error is returned.
  3. If none of the conditions match, then the `return` CTI trait is evaluated.
  4. While evaluating the expressions, all the dependencies (e.g. nested expressions) are resolved first

---

## Unhandled errors 

All the unhandled errors within the declarative code will be thrown by the Runtime system using
`cti.a.p.err.v1.0~a.p.dts.func.v1.0~a.p.unhandled.v1.0` error type.
The actual error will be encapsulated. The following error object fields will be filled:

| `cti.a.p.err.v1.0~a.p.dts.func.v1.0~a.p.unhandled.v1.0` Fields | Possible values                                                                          |
|----------------------------------------------|------------------------------------------------------------------------------------------|
| `source`                                     | The function that throws the error object                                                |
| `action`                                     | Actual action that caused the error                                                      |
| `request`                                    | Client's request ID                                                                      |
| `tenant`                                     | Client tenant ID – it's derived from the client's token                                  |
| `upstream_error?`                            | This is dependent on the `error_stack` attribute's value and only for authorized clients |


## Simplified and structured error handling primitive (`$result`)

**Key Requirements:** Function developers need simplified and structured way of trapping and handling errors in the declarative code. 

DTS Runtime offers the following functionality to simplify the error handling in the declarative function code:

Introduces a `$result(<dts_expression>)` resolver that allows executing a `<dts_expression>` and return the `Result` monad. The `Result` monad type is the following:
```yaml
  Result:
    properties:
      ok:
        type: boolean
        description: it will be true if underlying dts_expression was successful else value of false
      value?:
        type: any
        description: the value of the dts_expression if it was successful
      error?:
        type: object
        (cti.schema): cti.a.p.err.v1.0
        description: the error object if the dts_expression was not successful
```
	
2.	Consider below dts_expression examples:

  * `$cel(<dts_expression>)`  e.g. `$cel(.context.upstream.status_code != 200)`
  * `$func[:function_id](<function_arguments>)`  e.g. `$func[cti.a.p.dts.func.v1.0~a.p.my_func.v1.0]($.params.tenant_id)`
  * `$http[:route_id](<http_params>)`  e.g. `$http[a.p.igw.route.v1.0~a.p.account_server.v1.0](method='GET', path='/api/v2/tenants?tenantid=123')`
  * `$graphql_v1[:graphQL_id](<graphql_params>)`  e.g. `$graphql_v1[GetUser]($.params.user_id)` 
  * `$.context.upstream`
  * `$.params.tenant_id`
  * `$loop(...)`

3.	The attributes of the Result monad are directly accessible. 

4.	In case of success, the Result monad object will have below values

| Attribute name | Possible value   |
|----------------|------------------|
| `ok`           | `true`           |
| `value`        | `value \| any`   |
| `error`        | `null`           |

1. In case of error, the Result monad object will have below values

| Attribute name | Possible value                          |
|----------------|-----------------------------------------|
| `ok`           | `false`                                 |
| `value`        | `null`                                  |
| `error`        | error object of type `cti.a.p.err.v1.0` |

6. The `Result` Monad has following system-defined methods and equivalent resolvers.
The system functions are inspired from Rust's `Result<T,E>`'s method but it doesn't follow all the rules AS-IS hence read through carefully.

Method Functions:

| Function name                  | Value Resolver                            | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
|--------------------------------|-------------------------------------------| |
| is_ok()                        | _NA_                                      | returns `true` if Result is Ok i.e. `<Result_obj>.ok == true`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| is_err()                       | _NA_                                      | returns `true` if Result is Err i.e. `<Result_obj>.ok == false`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| unwrap_or_else(dts_expression) | `$unwrap_or_else(Result, dts_expression)` | Extract and return the `<Result_obj>.value` when `is_ok() == true` and return the result of provided `dts_expression` when `is_err() == true`  <br><br> **Please note:** <br> a. **Value resolver usage:** <br> i. If invalid parameters are passed to the value resolver `$unwrap_or_else(Result, dts_expression)` then it will: <br> &nbsp;&nbsp;&nbsp;&nbsp;1. throw error `cti.a.p.err.v1.0~a.p.dts.resolver.invalid_usage.v1.0` <br> &nbsp;&nbsp;&nbsp;&nbsp;2. with payload `cti.a.p.dts.resolver.err_details.v1.0~a.p.unwrap_or_else.invalid_usage.v1.0`                                                                                                         |
| and_then(dts_expression)       | `$and_then(Result, dts_expression)`       | Invoke the `dts_expression` when `<Result_obj>.is_ok() == true` <br><br> **Please note:** <br> a. **Member function usage:** <br> i. `<Result_obj>.and_then(dts_expression)` is skipped when `<Result_obj>.is_err() == true` <br> ii. You can chain multiple operations on the success case. <br> b. **Value resolver usage:** <br> i. If invalid parameters are passed to the value resolver `$and_then(Result, dts_expression)` then it will: <br> &nbsp;&nbsp;&nbsp;&nbsp;1. throw error `cti.a.p.err.v1.0~a.p.dts.resolver.invalid_usage.v1.0` <br> &nbsp;&nbsp;&nbsp;&nbsp;2. with payload `cti.a.p.dts.resolver.err_details.v1.0~a.p.and_then.invalid_usage.v1.0` |
| or_else(dts_expression)        | `$or_else(Result, dts_expression)`        | Invoke `dts_expression` when `<Result_obj>.is_err() == true` <br><br> **Please note:** <br> a. **Member function usage:** <br> i. `<Result_obj>.or_else(dts_expression)` is skipped when `<Result_obj>.is_ok() == true` <br> ii. You can chain multiple operations on the error case. <br> b. **Value resolver usage:** <br> i. If invalid parameters are passed to the value resolver `$or_else(Result, dts_expression)` then it will: <br> &nbsp;&nbsp;&nbsp;&nbsp;1. throw error `cti.a.p.err.v1.0~a.p.dts.resolver.invalid_usage.v1.0` <br> &nbsp;&nbsp;&nbsp;&nbsp;2. with payload `cti.a.p.dts.resolver.err_details.v1.0~a.p.or_else.invalid_usage.v1.0`          |

7. `$.self` value resolver is **only available within the member functions** of the `Result` monad, such as `or_else`, `and_then`, and `unwrap_or_else`. *Note:* When used within `$cel` its referred using `.self`

It refers to the `Result` object being operated on and provides access to its attributes (`ok`, `value`, and `error`).

**✅ Correct Usage:**

```yaml
$result(
  $http(route_id(...))
).or_else(
  $cel({
    "id": .context.uuid,
    "type": .self.error.type,
    "message": .self.error.message
  })
)
```

**❌ Incorrect Usage:**

```yaml
$and_then(
  $.context.upstream_result,
  $cel({
    "key": .self.value.some_property
  })
)
```

In this case, `$.self` is not available because `$and_then` is **not a member function** of the Result monad.
Instead, use the Result object directly (e.g., `$.context.upstream_result.value`).

> `and_then` and `or_else` are **value resolvers**, not member functions of the Result monad.
> As such, `$.self` is **not available** within these resolvers.

1. Member Function Input/Output Table

| Result's member functions | `self`        | Function input   | Function output | Notes                                                            |
|---------------------------|---------------|------------------|-----------------|------------------------------------------------------------------|
| `is_ok`                   | Result object | N/A              | boolean         |                                                                  |
| `is_err`                  | Result object | N/A              | boolean         |                                                                  |
| `unwrap_or_else`          | Result object | `dts_expression` | any             | Use `$.self.error` to work with the `<Result_obj>.error` object |
| `and_then`                | Result object | `dts_expression` | Result object   | Use `$.self.value` to work with the `<Result_obj>.value` object |
| `or_else`                 | Result object | `dts_expression` | Result object   | Use `$.self.error` to work with the `<Result_obj>.error` object |

> 📝 Note: `$.self` isn’t available in other value resolvers like `$or_else` or `$and_then` but **is accessible** within the Result's member function like `$result(...).or_else()`.


9. The Result object can originate from:

* `$result()` resolver
* `<Result_obj>.and_then(dts_expression)` system function
* `$and_then(Result, dts_expression)` resolver
* `<Result_obj>.or_else()` system function
* `$or_else(Result, dts_expression)` resolver

10. When operating on null values, DTS runtime will return:

* System error of type:
  `cti.a.p.err.v1.0~a.p.dts.func.v1.0~a.p.unhandled.v1.0`
* With payload of type:
  `cti.a.p.dts.err.details.unhandled.v1.0~a.p.null_value.v1.0`

## Simplified way for client to know the response type

**Key Requirements:** Clients should be able to know if response is success or error

1.	The Dispatcher's `/resolve` API response will provide the following additional details:

  a.	The Runtime will include optional `X-CTI-Body-Type` header attribute on the function response to the client. 
  b.	This header will exist only when DTS Function responds with an object of known CTI Type. For anonymous objects, this header will be missing.

2.	It allows the client to know the type of the object payload it receives in the HTTP response body.

---

### Consider below scenarios

| Function scenario/case                                     | Description                                                                                                                                                                                                                                                                                                                                                     | Return Type                                                                                                                                                                                                               |
|------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **No errors, well-known response type**                    | • The client is making the request to execute function `/api/dts/cti.a.p.dts.func.v1.0~a.p.my_func.v1.0`  <br> • The `~a.p.my_func.v1.0` function has `return_type` defined as CTI reference like `cti.x.y.z.v1.0`... <br> • The `~a.p.my_func.v1.0` function has been executed successfully                                                                    | **Status:** `200`<br> **Headers:**<br>`X-CTI-Body-Type: cti.x.y.z.v1.0`<br> **Body:**<br>`<some cti.x.y.z.v1.0 object>`                                                                                                   |
| **No errors, anonymous response type**                     | • The client is making the request to execute function `/api/dts/cti.a.p.dts.func.v1.0~a.p.my_func.v1.0`  <br> • The `~a.p.my_func.v1.0` function has failed because of an upstream error (e.g. failed upstream HTTP request)                                                                                                                                   | **Status:** `200`<br> **Body:**<br>`<return value>`                                                                                                                                                                       |
| **A runtime error detected (System errors)**               | • The client is making the request to execute function `/api/dts/cti.a.p.dts.func.v1.0~a.p.my_func.v1.0`  <br> • The `~a.p.my_func.v1.0` function has failed because of DTS runtime error (e.g. failed CEL expression)                                                                                                                                          | **Status:** `557`<br> **Headers:**<br>`X-CTI-Body-Type: cti.a.p.err.v1.0~a.p.dts.func.v1.0~a.p.unhandled.v1.0`<br> **Body:**<br>`<serverless runtime initializes and returns ~a.p.dts.func.v1.0~a.p.unhandled.v1.0 body>` |
| **A function returns its own error (User defined errors)** | • The client is making the request to execute function `/api/dts/cti.a.p.dts.func.v1.0~a.p.my_func.v1.0` <br> • The `~a.p.my_func.v1.0` function declared `cti.a.p.err.v1.0~example.app.my_func.v1.0` in function `error` attribute <br> • The `~a.p.my_func.v1.0` function returns `cti.a.p.err.v1.0~example.app.my_func.v1.0` (e.g. raised from Go-lang code) | **Status:** `555`<br> **Headers:**<br>`X-CTI-Body-Type: cti.a.p.err.v1.0~example.app.my_func.v1.0`<br> **Body:**<br>`<serverless runtime returns ~example.app.my_func.v1.0 coming from the error CTI trait>`              |


## Retriable Errors

**Key Requirements:** Client developers should know whether functions can be retried or not.

1.	Introduce the optional `retriable` CTI trait on all the errors. 
2.	If `retriable: true` on error types then clients can consider to retry the operation.

## Summary of all the Value Resolvers for Error Handling

| Additional Value Resolvers                     | Notes                                                                                                                                                            |
|------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `$result(dts_expression): Result`              | Lifts a computation (executing a `dts_expression`) into the `Result` structure (like Rust’s `Ok()` or `Err()`).                                                    |
| `$unwrap_or_else(Result, dts_expression): any` | Extract and return the `<Result_obj>.value` when `is_ok() == true` and in case `is_err() == true` then return the result of provided `dts_expression`.           |
| `$and_then(Result, dts_expression): Result`    | Invoke `dts_expression` when `<Result_obj>.is_ok() == true`.                                                                                                     |
| `$or_else(Result, dts_expression): Result`     | Invoke `dts_expression` when `<Result_obj>.is_err() == true`. It provides a way to handle or recover from an error. Skipped when `<Result_obj>.is_ok() == true`. |
| `$.self`                                       | The object instance passed to the member function of a Monad, implicitly, by the Runtime.                                                                        |
|                                                | At present, the `$.self` value resolver is only available within the **member functions** of the `Result` monad (e.g. `.or_else`, `.and_then`, `.unwrap_or_else`).    |
|                                                | It refers to the Result object being operated on and provides access to its attributes (`ok`, `value`, and `error`).                                             |

### Examples

#### `$result` usage

```json
$result(
  $http[cti.a.p.igw.route.v1.0~a.p.iam.v1.0](
    method='PUT',
    path=$cel('/tenants/' + .params.tenant_id + '/offering_items'),
    body={ "offering_items": $.params.offering_items },
    options={
      "allowed_status_codes": [200, 207],
      "timeout": "PT30S",
      "allowed_content_types": ["application/json"]
    }
  )
)
```

#### `or_else` usage

```json
$or_else(
  $.context.upstream_result,
  $cel({
    "id": .context.uuid,
    "type": .self.error.type,
    "message": .self.error.message,
    "payload": .self.error.payload
  })
)
```

