Docs Menu

Docs HomeDevelop ApplicationsAtlas Device SDKs

Handle Sync Errors - Kotlin SDK

On this page

  • Handle Sync Errors
  • Sync Exceptions
  • Unrecoverable Sync Errors
  • Wrong Sync Type Errors
  • Bad Flexible Sync Query Errors
  • Handle Client Reset Errors
  • Client Reset Strategies
  • Automatic vs. Manual Client Resets
  • Client Reset with Recovery
  • Specify a Client Reset Strategy
  • Recover or Discard Unsynced Changes
  • Recover Unsynced Changes
  • Discard Unsynced Changes
  • Manual Recovery Fallback
  • Manually Recover Unsynced Changes
  • Test Client Reset Handling

While developing an application that uses Device Sync, you should set an error handler. This error handler will detect and respond to any failed sync-related API calls.

Tip

For a list of common Device Sync errors and how to handle them, refer to Sync Errors in the App Services Device Sync documentation.

Set an error handler through the SyncConfiguration.errorHandler property when creating a synced realm. When an error occurs, the Kotlin SDK calls the error handler with the error object and the SyncSession that the error occurred on.

If you do not specify an error handler, the default behavior is to print the sync error to the console.

val syncErrorHandler = SyncSession.ErrorHandler { session, error ->
Log.e("Error message" + error.message.toString())
}
runBlocking {
val user = app.login(credentials)
val config = SyncConfiguration.Builder(user, setOf(Toad::class))
.initialSubscriptions { realm ->
add(realm.query<Toad>(), "subscription name")
}
.errorHandler(syncErrorHandler) // Specify a sync error handler
.build()
// Proceed to open a realm...
}

For information about setting a client log level or customizing the logger, refer to Set the Client Log Level - Kotlin SDK.

A SyncException is a subclass of AppException. A SyncException occurs when Device Sync fails.

For more information on app exceptions, refer to Handle App Errors.

An UnrecoverableSyncException occurs when Device Sync fails catastrophically. This usually means a bug in the client or connected App.

When an unrecoverable sync error occurs, you should surface the problem to the end user. Let them know that Device Sync won't work until the problem is solved. It's best to send yourself an alert so you can check the backend App logs and fix the problem as soon as possible.

A WrongSyncTypeException occurs when the client and App use different sync protocols.

The SDK supports two kinds of sync: flexible sync and partition based sync. When a client connects to an App using a sync type that does not match the App's sync type, a wrong sync type error occurs.

To recover from a wrong sync type error, update the client to use a sync type that matches the backend. This will most likely require the user to update to a new version of your app containing the fix.

A BadFlexibleSyncQueryException occurs when you try to subscribe to a flexible sync query that is not supported by the App backend. This can happen when you:

  • query a field not specified as a queryable field in your flexible sync configuration

  • create a flexible sync query using operators unsupported by flexible sync

To recover from a bad flexible sync query error, update your client to use a sync query compatible with your App configuration. This will most likely require the user to update to a new version of your app containing the fix.

When using Device Sync, a client reset is an error recovery task that your client app must perform when the Device Sync server can no longer sync with the client realm. In this case, the client must reset its realm to a state that matches the server in order to restore the ability to sync.

When this occurs, the unsyncable realm on the client may contain data that has not yet synced to the server. Realm SDKs can attempt to recover or discard that data during the client reset process.

For more information about what can cause a client reset to occur, go to Client Resets in the App Services documentation.

The Realm SDKs provide client reset strategies that automatically handle most client reset errors as well as a manual recovery strategy.

Automatic reset strategies restore your local realm file to a syncable state without closing the realm or missing notifications. The differences are based on how they handle changes on the device that have not yet synced to the backend. The following strategies implement the AutomaticClientResetStrategy interface and support automatic client resets:

  • Recover or discard unsynced changes (default): The client reset handler first attempts to recover unsynced changes. If recovery fails, this handler falls back to the discard unsynced changes strategy, which permanently deletes all unsynced local changes. If the discard unsynced changes strategy fails, the handler falls back to manual recovery.

  • Recover unsynced changes: The client reset handler first attempts to recover unsynced changes. If recovery fails, this handler falls back to manual recovery.

  • Discard unsynced changes: This strategy permanently deletes all local unsynced changes made since the last successful sync. If the discard fails, this handler falls back to manual recovery. This mode is recommended to handle any manual data recovery.

If your app requires specific client reset logic that can't be handled automatically, you may want or need to add a manual client reset handler using the ManuallyRecoverUnsyncedChangesStrategy interface:

  • Manually recover unsynced changes: Allows you to implement your own manual recovery strategy. Manual recovery is the only strategy that does not perform an automatic client reset. This mode allows you to backup your realm only. We recommend using the discard unsynced changes strategy to handle manual recovery if possible.

Client Recovery is a feature that is enabled by default when you configure Device Sync.

To use Client Recovery, configure your realm with the recover unsynced changes or recover or discard unsynced changes strategy, and Realm automatically manages the client reset process in most cases:

  • The client can recover unsynced changes when there are no schema changes or there are non-breaking schema changes.

  • Automatic client recovery cannot occur when your app makes breaking schema changes. A breaking change is a change that you can make in your server-side schema that requires additional action to handle. In this scenario, client recovery falls back to a manual error client reset fallback.

    For information on breaking vs. non-breaking schema changes, refer to Breaking vs. Non-Breaking Change Quick Reference in the App Services documentation.

Client Recovery Rules

When Client Recovery is enabled, these rules determine how objects are integrated, including how conflicts are resolved when both the backend and the client make changes to the same object:

  • Objects created locally that were not synced before client reset are synced.

  • If an object is deleted on the server, but is modified on the recovering client, the delete takes precedence and the client discards the update.

  • If an object is deleted on the recovering client, but not the server, then the client applies the server's delete instruction.

  • In the case of conflicting updates to the same field, the client update is applied.

You can specify a client reset strategy in your SyncConfiguration.syncClientResetStrategy property when configuring a synced realm.

// Specify your client reset strategy in the SyncConfiguration
// If you don't specify, defaults to RecoverOrDiscardUnsyncedChangesStrategy
val config = SyncConfiguration.Builder(user, setOf(Toad::class))
.initialSubscriptions { realm ->
add(realm.query<Toad>(), "subscription name")
}
.syncClientResetStrategy(clientResetStrategy) // Set your client reset strategy
.build()

The following sections describe how to use these client reset strategies.

The recover or discard unsynced changes strategy attempts to recover all unsynced local changes automatically during a client reset. To recover unsynced changes, Client Recovery must be enabled in your App Services App (it is enabled by default).

If the automatic recovery process fails, it falls back to a discard unsynced changes strategy. If that process process fails, it falls back again to a manual reset strategy.

This strategy provides the most robust recovery process. It is the default client reset behavior if you do not specify a client reset strategy.

Important

Do not use the recover or discard unsynced changes strategy if your application cannot lose any local data that has not yet synced to the backend.

To customize usage of recover or discard unsynced changes strategy, define a class implementing the RecoverOrDiscardUnsyncedChangesStrategy interface.

The interface provides the following callback methods:

  • onBeforeReset: Invoked prior to the client reset. Provides an instance of the realm before the reset. You can use this callback to notify the user before the client reset begins.

  • onAfterRecovery: Invoked if and only if the automatic reset completes successfully. Provides a read-only before instance of the realm and a mutable instance of the final realm. You can use this callback to notify the user that the client reset is complete.

  • onAfterDiscard: Invoked only if the automatic client reset fails and the discard local strategy succeeds. If the discard strategy fails, this callback is not invoked. Provides a read-only before instance of the realm and a mutable instance of the final realm. You can use this callback to notify the user that the reset is complete.

  • onManualResetFallback: Invoked only if the automatic recovery and the discard strategy have failed. Discards the local changes and allows you to create backup of the local realm. Implement this callback to handle the reset failure, as explained in the Manual Recovery Fallback section.

The following example shows the RecoverOrDiscardUnsyncedChangesStrategy and each of its callbacks:

val clientResetStrategy = object : RecoverOrDiscardUnsyncedChangesStrategy {
override fun onBeforeReset(realm: TypedRealm) {
Log.i("Client reset: attempting to automatically recover unsynced changes")
}
// Executed before the client reset begins.
// Can be used to notify the user that a reset will happen.
override fun onAfterRecovery(before: TypedRealm, after: MutableRealm) {
Log.i("Client reset: successfully recovered all unsynced changes")
}
// Executed if and only if the automatic recovery has succeeded.
override fun onAfterDiscard(before: TypedRealm, after: MutableRealm) {
Log.i("Client reset: recovery unsuccessful, attempting to manually recover any changes")
// ... Try to manually recover any unsynced data
manuallyRecoverUnsyncedData(before, after)
}
// Executed if the automatic recovery has failed,
// but the discard unsynced changes fallback has completed successfully.
override fun onManualResetFallback(
session: SyncSession,
exception: ClientResetRequiredException
) {
Log.i("Client reset: manual reset required")
// ... Handle the reset manually here
}
// Automatic reset failed.
}

The recover unsynced changes strategy attempts to recover all unsynced local changes automatically during a client reset. To recover unsynced changes, Client Recovery must be enabled in your App Services App (it is enabled by default).

However, unlike the recover and discard unsynced changes strategy, it does not fall back to discard local changes if the automatic recovery fails. Instead, it falls back to manually recover changes. You might choose this client reset strategy if your app cannot lose unsynced data.

To use the recover unsynced changes strategy, define a handler implementing the RecoverUnsyncedChangesStrategy interface.

The interface provides the following callback methods:

  • onBeforeReset: Invoked prior to the client reset. Provides an instance of the realm before the reset. You can use this callback to notify the user before the client reset begins.

  • onAfterReset: Invoked if and only if the automatic reset completes successfully. Provides a read-only before instance of the realm and a mutable instance of the final realm. You can use this callback to notify the user that the client reset is complete.

  • onManualResetFallback: Invoked only if the automatic recovery has failed. Discards the local changes and allows you to create backup of the local realm. Implement this callback to handle the reset failure, as explained in the Manual Recovery Fallback section.

The following example shows the RecoverUnsyncedChangesStrategy and each of its callbacks:

val clientResetStrategy = object : RecoverUnsyncedChangesStrategy {
override fun onBeforeReset(realm: TypedRealm) {
Log.i("Client reset: attempting to automatically recover unsynced changes")
}
// Executed before the client reset begins.
// Can be used to notify the user that a reset will happen.
override fun onAfterReset(before: TypedRealm, after: MutableRealm) {
Log.i("Client reset: successfully recovered all unsynced changes")
}
// Executed after the client reset is complete.
// Can be used to notify the user that the reset is done.
override fun onManualResetFallback(
session: SyncSession,
exception: ClientResetRequiredException
) {
Log.i("Client reset: manual reset required")
// ... Handle the reset manually here
}
// Automatic reset failed.
}

The discard unsynced changes strategy permanently deletes all local unsynced changes made since the last successful sync. This strategy restores your local realm file to a syncable state without closing the realm and while keeping notifications fully working. If this process fails, it falls back to a manual reset strategy.

This is the recommended strategy to handle any manual data recovery.

You might choose this strategy when your app requires client recovery logic that is not consistent with the Device Sync Client Recovery Rules or when you don't want to recover unsynced data.

Important

Do not use the discard unsynced changes strategy if your application cannot lose any local data that has not yet synced to the backend.

To use the discard unsynced changes strategy, define a handler implementing the DiscardUnsyncedChangesStrategy interface.

The interface provides the following callback methods:

  • onBeforeReset: Invoked prior to the client reset. Provides an instance of the realm before the reset. You can use this callback to notify the user before the client reset begins.

  • onAfterReset: Invoked if and only if the reset completes. Provides a read-only before instance of the realm and a mutable instance of the final realm. You can use this callback to notify the user that the reset is complete.

  • onManualResetFallback: Invoked only if the automatic recovery and the discard strategy have failed. Discards the local changes and allows you to create backup of the local realm. Implement this callback to handle the reset failure, as explained in the Manual Recovery Fallback section.

The following example shows the DiscardUnsyncedChangesStrategy and each of its callbacks:

val clientResetStrategy = object : DiscardUnsyncedChangesStrategy {
override fun onBeforeReset(realm: TypedRealm) {
Log.i("Client reset: attempting to discard any unsynced changes")
}
// Executed before the client reset begins.
// Can be used to notify the user that a reset will happen.
override fun onAfterReset(before: TypedRealm, after: MutableRealm) {
Log.i("Client reset: attempting to manually recover any unsynced changes")
// ...Try to manually recover any unsynced data
manuallyRecoverUnsyncedData(before, after)
}
// Executed after the client reset is complete.
// Can be used to notify the user that the reset is done.
override fun onManualResetFallback(
session: SyncSession,
exception: ClientResetRequiredException
) {
Log.i("Client reset: manual reset required")
// ... Handle the reset manually here
}
// Automatic reset failed.
override fun onError(
session: SyncSession,
exception: ClientResetRequiredException
) {
// No-op
}
// Deprecated. onManualResetFallback() used instead.
}

If the client reset cannot complete automatically, such as when there are breaking schema changes, the client reset process falls through to a manual error handler.

This may occur in any of the automatic client reset strategies:

  • Recover unsynced changes

  • Recover or discard unsynced changes

  • Discard unsynced changes

You must provide a manual client reset implementation in the onManualResetFallback callback of the client reset handler for these strategies.

The manual reset fallback discards the local changes and allows you to create backup of the local realm using the ClientResetRequiredException.executeClientReset method.

override fun onManualResetFallback(
session: SyncSession,
exception: ClientResetRequiredException
) {
Log.i("Client reset: manual reset required")
// You *MUST* close any open Realm instance
closeAllRealmInstances();
// `executeClientReset()` creates a backup
exception.executeClientReset();
// (Optional) Send backup for analysis
handleBackup(recoveryFilePath);
// ... Restore the App state by reopening the realm
// or restarting the app
}

Use the manually recover unsynced changes strategy for the infrequent cases where you need to customize your data recovery process. We recommend using the Discard unsynced changes strategy when possible to handle manual data recovery. You should only choose the manually recover unsynced changes strategy if the automatic recovery logic is not suitable for your app and you can't discard unsynced local data.

To use the manual recovery strategy, define your own client reset handler using the ManuallyRecoverUnsyncedChangesStrategy interface.

Before you use this method, you must close all instances of the realm that you are resetting. Any writes to the realm file made after the manual recovery callback and before the client reset being executed will not be synced. We also recommend that you create a backup of the file and report the exception.

Initiate a client reset using ClientResetRequiredException.executeClientReset().

If the client reset isn't executed manually, it will be automatically executed the next time all realm instances have been closed and re-opened (typically when the app is restarted).

You can manually test your application's client reset handling by terminating and re-enabling Device Sync.

When you terminate and re-enable Sync, clients that have previously connected with Sync are unable to connect until after they perform a client reset. Terminating Sync deletes the metadata from the server that allows the client to synchronize. The client must download a new copy of the realm from the server. The server sends a client reset error to these clients. So, when you terminate Sync, you trigger the client reset condition.

To test client reset handling:

  1. Write data from a client application and wait for it to synchronize.

  2. Terminate and re-enable Device Sync.

  3. Run the client app again. The app should get a client reset error when it tries to connect to the server.

Warning

While you iterate on client reset handling in your client application, you may need to terminate and re-enable Sync repeatedly. Terminating and re-enabling Sync renders all existing clients unable to sync until after completing a client reset. To avoid this in production, test client reset handling in a development environment.

← Manage a Sync Session - Kotlin SDK