# Web5 JS Error Handling
## Potential Approach
### Fetch API
We discussed modeling the approach in the Web5 JS packages and `dwn-server` after how the WHATWG Fetch API handles HTTP status codes.
Here's a simplified explanation of how the Fetch API handles HTTP status codes:
1. **Successful responses (200-299)**: For these status codes, the Fetch API returns a Response object wrapped in a resolved Promise. The Response object has several methods for accessing the body of the response in different formats (.text(), .json(), .blob(), etc.). The status code of the response can be checked using the **`.status`** property of the Response object.
2. **Redirects (300-399)**: These status codes are typically used for redirection. When you make a request using the Fetch API, by default, it follows HTTP redirects, that is, if it receives a response with a status code in the 300-399 range, it will automatically make a new request to the URL specified in the Location header of the response. Currently there is no use of any 3XX range codes in any of the Web5/DWN SDKs.
3. **Error responses (400-599):** The behavior is similar to successful responses. The Fetch API will still return a Response object wrapped in a resolved Promise, not a rejected one. The Response object can be used to check the status code, status message and other metadata about the response.
The key point to understand is that the Fetch API does **not** consider a HTTP error status (like 404 or 500) as a network error. It only considers situations where the request cannot complete as network errors. These situations could be due to reasons like:
* The network is down.
* The user is offline.
* The URL is not a valid URL.
* A request is made to a URL which the user agent (like a web browser) doesn't have permission to access due to security reasons, like the Same Origin Policy or CORS.
In such cases, the Fetch API will reject the Promise with a **`TypeError`**.
Here is a simple example:
```javascript
fetch('https://example.com/some-api-endpoint')
.then(response => {
if (!response.ok) {
// handle the error as appropriate for the application, which could
// be throwing an exception.
}
return response.json();
})
.then(data => {
// do something with the data
})
.catch(error => {
console.log('There was a problem with the fetch operation: ' + error.message);
});
```
or using `await`:
```javascript
try {
const response = await fetch('https://example.com/some-api-endpoint')
if (!response.ok) {
// handle the error as appropriate for the application, which could
// be throwing an exception.
}
const data = await response.json();
} catch (error) {
console.log('There was a problem with the fetch operation: ' + error.message);
}
```
### DWN API
If we were to follow a similar approach to the Fetch API for response error handling, `dwn-server` and `Web5Agent` might:
- Return a DWN Response object wrapped in a *resolved* Promise for DWN response status codes in the 400-599 range. These would **NOT** be considered transport errors between:
- `Web5UserAgent <-> agent's embedded DWN`
- `Web5ProxyAgent <-> Web5UserAgent`
- `Web5UserAgent <-> dwn-server`
- `Web5ProxyAgent <-> Web5UserAgent <-> dwn-server`
- Only situations where the request cannot complete are considered transport errors that should result in *rejecting* the Promise. These include but are not limited to:
- A request using a network transport protocol (e.g., HTTP/S, WS, etc.) is made but the network connection is down. A common example would be no connection to the Internet while in a remote area or on a plane without in-flight WiFi service.
- The DWN Request is malformed such that the request cannot complete. Examples include failing to provide required properties in a DWN Request message or providing an unparseable DWN Request message (e.g., number input where string was expected).
- The DID input is invalid or cannot be resolved. Examples could include providing the short form of an ION DID prior to being anchored, providing an invalid DID as input (e.g., `record.send('did:foo:bar'`), attempting to write a message to a DID whose DID document does not contain a DWN service entry, or attempting to encrypt a record to a recipient whose DID document does not contain the `recordEncryptionKeys` property AND the developer did not specify a specific `keyId` to use.
- With the exception of the above cases where the Promise is rejected, ALWAYS return a **`status`** property in the response object with the following structure:
```javascript
status: { code: number, detail: string }
```
- This enables the caller to have sufficient information in the response object regarding the outcome of their request and take action accordingly. We cannot predict the appropriate error handling for every use case, but by resolving the Promise and providing the response status and metadata, an application being built using the Web5/DWN libraries can act accordingly.
### Suggestion to mimic Fetch's `response.ok` affordance
The concept of not throwing an exception when a 404 or 400 occurs might be confusing to developers initially. In fact, we ourselves had instinctively wanted to return JSON RPC error responses in `dwn-server` whenever the DWN SDK returned a 400-599 error.
To attempt to minimize the concern / confusion perhaps we adopt the Fetch API convention of providing an **`ok`** property on all of the Web5 JS response objects? That way developers looking at our code internally or building applications using the libraries would be checking `if (response.ok)` as opposed to having to make the mental leap to realize that the request should be considered successfully processed EVEN IF the actual outcome is that the DWN 0, 1, or 2 hops away returned by a 404 because the `recordId` wasn't found or a 409 because you accidentally tried to write a duplicate record.
- `response.ok === true` as long as the request was comleted
- `response.ok === false` if one of the aforementioned reasons for rejecting the Promise occurred.
### Exposing vs. Wrapping / Unwrapping JSON RPC messages.
This hasn't been entirely thought through and I haven't checked every JSON RPC handoff on the request and response path to verify whether this might already be done. There might also be very good reasons NOT to do this but suggesting anyways to ensure its either already done this way consistently everywhere or that theres a reason not to.
Rather than exposing the nuances of JSON RPC messages to the Web5 components that communicate over network boundaries using JSON RPC, what if we normalized the responses just like we do the requests?
For example, when request options are passed in to `web5.dwn.records.write()`, `.query()`, etc., they end up being wrapped in a JSON RPC Request object that follows the format of the JSON RPC 2.0 spec. However, we don't expect the caller to know that or do anything special to conform that by providing an `id` value, specifying a `method`, nor placing the request options within `params`.
In the same way, why shouldn't we "unwrap" the JSON RPC Response object so that none of the upstream callers have to know about the specific structure of JSON RPC Response Success vs. Error objects. For example, Response Success object contain a `result` with the `status` object within `reply` while Response Error objects contain an `error` object with the the status code accessed as `error.code` and status detail in `error.message`. There's also an additional wrinkle that the `error.code` in the case of a JSON RPC Error response will be in the range -32000 to -50999 whereas the DWN status code is embedded in `error.data.code`.
I * think * but not 100% sure that it would be ideal if every Response object that a Web5 Agent interacts with always has:
`response.ok: true | false`
`response.status: { code: number, detail: string}`
and then any additional properties that are relevant for the specific response type (e.g., `message`, `record`, `records`).
## DWN SDK Response Codes
### `RecordsWrite`
**`SUCCESS`**
- `{ status: { code: 202, message: 'Accepted' }`
- query successfully processed and 0 or more results returned in `entries`
**`FAILURE`**
- `{ status: { code: 400, message: '???' }`
- parsing the RecordsDeleteMessage failed
- `{ status: { code: 400, message: '???' }`
- attempted to modify immutable properties
- `{ status: { code: 400, message: '???' }`
- writing to the message store threw an error and error was one of:
- DwnErrorCode.StorageControllerDataCidMismatch
- DwnErrorCode.StorageControllerDataNotFound
- DwnErrorCode.StorageControllerDataSizeMismatch
- `{ status: { code: 401, message: '???' }`
- either:
- authentication of `message.authorization` failed
- authorization failed
- `{ status: { code: 409, message: 'Conflict' }`
- incoming message is not the "newest" in the DWN
---
### `RecordsRead`
**`SUCCESS`**
- `{ status: { code: 200, message: 'OK' }`
- query successfully processed and 0 or more results returned in `entries`
**`FAILURE`**
- `{ status: { code: 400, message: '???' }`
- parsing the RecordsDeleteMessage failed
- `{ status: { code: 401, message: '???' }`
- authentication of `message.authorization` failed
- `{ status: { code: 404, message: 'Not found' }`
- either:
- `recordId` not found in message store
- `recordId` was previously deleted
---
### `RecordsQuery`
**`SUCCESS`**
- `{ status: { code: 200, message: 'OK' }`
- query successfully processed and 0 or more results returned in `entries`
**`FAILURE`**
- `{ status: { code: 400, message: '???' }`
- parsing the RecordsDeleteMessage failed
- `{ status: { code: 401, message: '???' }`
- either:
- authentication of `message.authorization` failed
- authorization failed
---
### `RecordsDelete`
**`SUCCESS`**
- `{ status: { code: 202, message: 'Accept' }`
- `recordId` exists and delete request was accepted.
- `{ status: { code: 202, message: 'Accept' }`
- `recordId` was previously deleted
- `{ status: { code: 202, message: 'Accept' }`
- `recordId` has never existed
**`FAILURE`**
- `{ status: { code: 400, message: '???' }`
- parsing the RecordsDeleteMessage failed
- `{ status: { code: 401, message: '???' }`
- either:
- authentication of `message.authorization` failed
- authorization failed because message `author !== tenant`
- `{ status: { code: 409, message: 'Conflict' }`
- incoming message is not the "newest" in the DWN
---