Skip to main content

Errors and rejections

Swan added the Rejection type into the API due to how GraphQL chose to handle errors.

GraphQL errors

In GraphQL, all the errors, or rather non-nominal responses, go through the errors field, which contains a list of non-typed objects. Because errors aren't strongly typed like the rest of the GraphQL schema, there's no guaranteed format or structure to error responses.

This is by design and part of GraphQL's specifications.

Swan's responsibility

It's Swan's responsibility as a financial institution and a business to make sure responses to API calls that request modifications (mutations) can be thoroughly reviewed and understood by all API consumers.

As a result, Swan implemented the Rejection type in the API instead of relying on GraphQL's default, un-typed errors field to provide you with as much information as possible about failed mutations.

Swan API errors

Adding Rejections doesn't mean the error fields disappeared from Swan's API.

Rejections are implemented for all mutations with anticipated errors caused by business rules. For unexpected errors, and all query errors, the API returns a classic GraphQL error field.

Rejection object

A Rejection is a GraphQL type returned by mutations—and only mutations—from the Swan API when a request is rejected due to a business rule.

Object structure

In the Swan schema, a Rejection is an object containing a descriptive message.

Rejection structure
interface Rejection {
message: String!
}

Each business rule that can provoke a Rejection will have a type extending the Rejection interface. For example, if you try to access a consent that doesn't exist, you receive a Rejection in return, described like this, which also includes the consentId:

Consent not found rejection
type ConsentNotFoundRejection implements Rejection {
message: String!
consentId: String!
}

Union: success or rejection

Therefore, all Swan mutations are either successful or return a Rejection. Swan implemented this concept using a GraphQL union.

Consider the cancelConsent mutation:

Cancel consent
type Mutation {
cancelConsent(input: CancelConsentInput!): CancelConsentPayload!
}

union CancelConsentPayload = CancelConsentSuccessPayload | ConsentNotFoundRejection

type CancelConsentSuccessPayload {
consent: Consent!
}

A canceled consent returns the CancelConsentPayload. However, if the consent couldn't be found, the API returns the ConsentNotFoundRejection.

Extract possible rejections

In the Swan Banking Frontend open source project, there is a script to extract every object that implements a Rejection. The script then compiles the list of objects in a locales file with all rejection keys (rejection.{name}).

You can download the extractRejections TypeScript file file from GitHub.

Using rejections

You can add rejections to all mutation calls, and Swan encourages you to do so. In fact, all API mutation examples included in the documentation feature rejections.

bug

Rejections aren't available for a small subset of fields linked to a specific backend service. Swan developers are working on fixing the issue.

Continuing with the cancelConsent example, consider how Swan handles different Rejection cases.

While this example is basic, consider how helpful the additional information is in each rejection payload.

No rejection added

The following code block illustrates a mutation fetching a consent, but only a success payload is added to the call (line 3), not a Rejection.

Note that the success payload looks good: the consent ID is returned as requested (line 17).

However, the rejection payload contains no information (lines 27).

No rejection added
mutation NoRejection {
cancelConsent(input: {consentId: "$YOUR_CONSENT_ID"}) {
... on CancelConsentSuccessPayload {
consent {
id
}
}
}
}

# Success payload

{
"data": {
"cancelConsent": {
"consent": {
"id" : "$YOUR_CONSENT_ID"
}
}
}
}

# Rejection payload

{
"data": {
"cancelConsent": {}
}
}

Add basic rejection

Now, add a basic rejection to the same cancelConsent mutation.

The success payload still looks good (line 20).

This time, though, the API provides a reason in the rejection payload: the consent ID wasn't found (lines 31).

No rejection added
mutation BasicRejection {
cancelConsent(input: {consentId: "$YOUR_CONSENT_ID"}) {
... on CancelConsentSuccessPayload {
consent {
id
}
}
... on Rejection {
message
}
}
}

# Success payload

{
"data": {
"cancelConsent": {
"consent": {
"id" : "$YOUR_CONSENT_ID"
}
}
}
}

# Rejection payload

{
"data": {
"cancelConsent": {
"message": "Consent with id '$YOUR_CONSENT_ID' was not found."
}
}
}

Add detailed rejection

Add a detailed rejection to the same cancelConsent mutation (line 9).

Again, the success payload looks good (line 24).

Now, the API provides more information about why the mutation was rejected, including the consent ID you were looking for, the __typename, and that the consent ID wasn't found (lines 35-37).

No rejection added
mutation MyMutation {
cancelConsent(input: {consentId: "$YOUR_CONSENT_ID"}) {
... on CancelConsentSuccessPayload {
consent {
id
}
}
... on Rejection {
... on ConsentNotFoundRejection{
__typename
consentId
}
message
}
}
}

# Success payload

{
"data": {
"cancelConsent": {
"consent": {
"id" : "$YOUR_CONSENT_ID"
}
}
}
}

# Rejection payload

{
"data": {
"cancelConsent": {
"__typename": "ConsentNotFoundRejection",
"consentId": "$YOUR_CONSENT_ID",
"message": "Consent with id '$YOUR_CONSENT_ID' was not found."
}
}
}