# Handle errors

Handle transaction, communication, and device errors in your Tap to Pay on iPhone integration, and resolve a sale with an unknown outcome after a communication failure.

**Prerequisites:** [Authentication](/api/authentication) · [Run a sale](/guides/payments/tap-to-pay-on-iphone/run-a-sale)

The SDK reports errors through different callbacks depending on the context. There are three main types of error that the SDK can return:

| **Type** | **Callback** | **Listener** |
| --- | --- | --- |
| [Transaction errors](#transaction-errors) | `onError:withDescription:` | `CoreAPISaleListener` |
| [Communication errors](#communication-errors) | `onTransactionCommError:withError:withMessage:` | `CoreAPISaleListener` |
| [Device errors](#device-errors) | `onDeviceError:withDescription:` | `CoreAPIDeviceListener` |

<a id="transaction-errors"></a>
## Transaction errors

The SDK returns transaction processing failures, such as network problems, to `onError:withDescription:` on `CoreAPISaleListener`. The `CoreError` enumeration identifies the type of failure.

**`Swift`**

```swift
func onError(_ error: CoreError, withDescription message: String!) {
    print("Sale error \(error.rawValue): \(message ?? "")")
}
```

**`Objective-C`**

```objectivec
- (void)onError:(CoreError)error withDescription:(NSString *)message {
    NSLog(@"Sale error %d: %@", (int)error, message);
}
```

**Note:** `CoreAPIRefundListener` uses the same parameters for refund errors.

For the full list of `CoreError` values, go to the `CoreError` page in the SDK reference.

<a id="communication-errors"></a>
## Communication errors

**Important:** If you receive a communication error, treat the sale as unresolved. If we authorized the sale, we recommend that you reverse the sale.

A communication error happens when the device reads the card successfully but the transaction request doesn't reach our gateway. Because the customer has already tapped their card, you don't know the outcome on our gateway.

The SDK returns communication errors to `onTransactionCommError:withError:withMessage:` on `CoreAPISaleListener`, and passes back the original `CoreSale` object.

**Note:** Referenced refunds don't have a dedicated communication error callback. If a referenced refund fails to reach our gateway, the SDK reports it through `onError:withDescription:` on `CoreAPIRefundListener` instead.

We recommend that you use the following recovery process for sales:

1. Store the `CoreSale` object on the device.
2. When the device is back online, use `RestConnector.requestTransactionList` to search for the transaction by `orderId` or by the last four digits of the card.
3. Check the result:
   
   - `code` is `A`: we authorized the transaction.
   - `code` not `A`: we declined the transaction. There's nothing to reverse.
   - No transaction: the transaction didn't reach our gateway. There's nothing to resolve, and you can run the sale again.
4. If we authorized the sale, we recommend that you reverse it. Create a `CoreReversal` with the `uniqueRef` of the transaction, and then send it to `RestConnector.request` with `isRefund` set to `NO`.
5. (Optional) Run the sale again.

The following example checks the status of the sale and resolves it when the device is back online:

**`Swift`**

```swift
func onTransactionCommError(_ sale: CoreSale!, withError error: CoreError, withMessage message: String!) {
    // Save the sale to disk so that it survives an app restart.
    savePendingSale(sale.convertToDictionary())
}

// Call this from a background thread when the device is back online.
func resolvePendingSale(_ sale: CoreSale) {
    let filter = CoreTransactionFilter()
    filter.orderId = sale.orderId
    filter.panLastFour = String(sale.maskedPAN.suffix(4))

    do {
        let result = try RestConnector.requestTransactionList(filter, with: terminal)

        guard let match = result.transactionSummary?.first as? CoreTransactionSummary else {
            // The transaction didn't reach our gateway. You can run the sale again.
            return
        }

        guard match.code == "A" else {
            // We declined the transaction. There's nothing to reverse.
            return
        }

        // We authorized the transaction. Reverse it and show a receipt.
        let reversal = CoreReversal()
        reversal.deviceType = "AppleTTP"
        reversal.uniqueRef = match.uniqueRef
        try RestConnector.request(reversal, isRefund: NSNumber(value: false), with: terminal)
    } catch {
        // The request failed. Try again later.
    }
}
```

**`Objective-C`**

```objectivec
- (void)onTransactionCommError:(CoreSale *)sale
                     withError:(CoreError)error
                   withMessage:(NSString *)message {
    // Save the sale to disk so that it survives an app restart.
    [self savePendingSale:[sale convertToDictionary]];
}

// Call this from a background thread when the device is back online.
- (void)resolvePendingSale:(CoreSale *)sale {
    NSError *error = nil;
    CoreTransactionFilter *filter = [[CoreTransactionFilter alloc] init];
    filter.orderId = sale.orderId;
    filter.panLastFour = [sale.maskedPAN substringFromIndex:sale.maskedPAN.length - 4];

    CoreTransactions *result = [RestConnector requestTransactionList:filter
                                                        withTerminal:self.terminal
                                                               error:&error];
    if (error != nil) {
        // The request failed. Try again later.
        return;
    }
    if (result.transactionSummary.count == 0) {
        // The transaction didn't reach our gateway. You can run the sale again.
        return;
    }

    CoreTransactionSummary *match = result.transactionSummary.firstObject;
    if (![match.code isEqualToString:@"A"]) {
        // We declined the transaction. There's nothing to reverse.
        return;
    }

    // We authorized the transaction. Reverse it and show a receipt.
    CoreReversal *reversal = [[CoreReversal alloc] init];
    reversal.deviceType = @"AppleTTP";
    reversal.uniqueRef = match.uniqueRef;
    [RestConnector requestReversal:reversal
                          isRefund:[NSNumber numberWithBool:NO]
                      withTerminal:self.terminal
                             error:&error];
}
```

<a id="device-errors"></a>
## Device errors

The SDK returns device errors to `onDeviceError:withDescription:` on `CoreAPIDeviceListener`. The `CoreDeviceError` enumeration covers device failures, entitlement problems, and hardware faults.

**`Swift`**

```swift
func onDeviceError(_ deviceError: CoreDeviceError, withDescription message: String!) {
    print("Device error \(deviceError.rawValue): \(message ?? "")")
}
```

**`Objective-C`**

```objectivec
- (void)onDeviceError:(CoreDeviceError)deviceError withDescription:(NSString *)message {
    NSLog(@"Device error %d: %@", (int)deviceError, message);
}
```

The SDK includes a set of `CoreDeviceError` values that are specific to Apple Tap to Pay, and they all start with `APPLE_TTP_`. They cover problems such as an unsupported iPhone model, an unsupported iOS version, a disabled passcode, a blocked merchant, and account-linking failures. For the full list, go to the `CoreDeviceError` page in the SDK reference.

If the device encounters an error and becomes unavailable, `onDeviceDisconnected:` fires.

**`Swift`**

```swift
func onDeviceConnected(_ type: DeviceEnum, withDeviceInfo deviceInfo: [AnyHashable: Any]!) {
    // The device is active and ready
}

func onDeviceDisconnected(_ type: DeviceEnum) {
    // The reader is unavailable. Disable your payment button.
}
```

**`Objective-C`**

```objectivec
- (void)onDeviceConnected:(DeviceEnum)type withDeviceInfo:(NSDictionary *)deviceInfo {
    // The device is active and ready
}

- (void)onDeviceDisconnected:(DeviceEnum)type {
    // The reader is unavailable. Disable your payment button.
}
```
