Skip to main content

Error Handling

AFCore uses two primary error-handling patterns: AFResult for operations that return a success/failure result, and thrown AFException for operations that fail at the network or system level. This page covers both patterns, common error scenarios, and recommended handling approaches.

Two Error Patterns

Pattern 1: AFResult (Result-Based)

Some APIs return AFResult, which encapsulates both success and failure as a data object. The caller checks result.status to determine the outcome:

val result = AFCore.authentication().signInOrCreateMember(externalUserId = userId)
if (result.status) {
// Success
} else {
// Business logic failure
Log.w("Auth", result.statusMessage)
}

Pattern 2: Thrown AFException (Exception-Based)

All suspend functions can throw AFException (or its parent Exception) when a network, server, or unexpected error occurs. These must be caught with try/catch (Kotlin) or do/catch (Swift):

try {
val profile = AFCore.profile().get()
} catch (e: AFException) {
// Network error, server error, or unexpected error
Log.e("Profile", "Failed: ${e.message}")
}

Which APIs Use Which Pattern

API CategoryPatternExample
Authentication (signInOrCreateMember, logout)Both: Returns AFResult for business outcomes, throws AFException for network/system errorsAFResult.status == false for auth failure; AFException for network timeout
Data retrieval (profile().get(), activities().get(), facilities().get())Exception-based: Returns data on success, throws on failureReturns MemberProfile; throws AFException on error
Data mutation (barcode().submitBarcodeId(), profile().update(), smartWalking().syncSteps())Exception-based: Returns data on success, throws on failureReturns response; throws AFException on error
Event submission (sendEvent, postEvent)Exception-based: Completes silently on success, throws on failureThrows AFException on error

AFResult

AFResult is the standard response wrapper for operations that can fail for business logic reasons (not just technical failures):

public class AFResult(
public var status: Boolean = false,
public var statusMessage: String? = null,
public var messageCode: String? = null,
public var exception: AFException? = null
)
FieldTypeDescription
statusBooleantrue if the operation succeeded, false if it failed
statusMessageString?Human-readable description of the result (suitable for displaying to users)
messageCodeString?Machine-readable code for programmatic handling
exceptionAFException?Populated if an AFException occurred during the operation

Handling AFResult

val result = AFCore.authentication().signInOrCreateMember(externalUserId = userId)

when {
result.status -> {
// Operation succeeded
navigateToHome()
}
result.exception != null -> {
// An error exception is attached
val ex = result.exception!!
Log.e("Auth", "Error: ${ex.message}")
Log.e("Auth", "Detail: ${ex.detail}")
showError(ex.message ?: "An error occurred")
}
else -> {
// Business logic failure (status = false, no exception)
showError(result.statusMessage ?: "Operation failed")

// Use messageCode for programmatic branching
when (result.messageCode) {
"MEMBER_NOT_ELIGIBLE" -> showEligibilityError()
else -> showGenericError(result.statusMessage)
}
}
}

AFException

AFException is the SDK's extended exception type. It carries structured error information from the server:

public class AFException(
public override val message: String? = null,
public val isError: Boolean? = null,
public val detail: String? = null,
public val data: String? = null,
public val errors: List<String?>? = null,
public val httpCode: Int? = null
) : Exception()
FieldTypeDescription
messageString?Primary error message
isErrorBoolean?Whether this represents an error condition (from server response)
detailString?Additional details about the error
dataString?Additional data related to the error
errorsList<String?>?List of individual error messages (e.g., validation errors)
httpCodeInt?HTTP status code from the server response (e.g., 400, 404, 500)

How AFException Is Created

The safeApiCall{} function wraps all API calls and converts Ktor exceptions into AFException:

Source ExceptionAFException.messageWhen It Occurs
ClientRequestException (4xx)Parsed from server error response bodyBad request, unauthorized, forbidden, not found
ServerResponseException (5xx)Parsed from server error response bodyInternal server error, service unavailable
JsonConvertException"Invalid or unexpected response format: {details}"Response body cannot be deserialized
NoTransformationFoundException"Invalid or unexpected response format"No serializer found for the response type
IOException"Network error. Please try again later."No internet, DNS failure, connection refused, timeout
Other Exception"Unexpected error: {message}"Any other unexpected failure
CancellationExceptionRe-thrown (not caught)Coroutine was cancelled (normal behavior)

For 4xx and 5xx errors, the safeApiCall{} function attempts to parse the server's error response body into a structured AFException with message, isError, detail, data, and errors fields populated from the JSON response.

Error Handling by API Category

Authentication Errors

Authentication methods return AFResult and can also throw exceptions:

lifecycleScope.launch {
try {
val result = AFCore.authentication().signInOrCreateMember(externalUserId = userId)

if (result.status) {
navigateToHome()
} else {
// Business failure: member not eligible, etc.
showError(result.statusMessage ?: "Authentication failed")
}
} catch (e: AFException) {
// Network or server error
handleNetworkError(e)
}
}

private fun handleNetworkError(e: AFException) {
when {
e.message?.contains("Network error") == true ->
showError("No internet connection. Check your network and try again.")
e.message?.contains("Server error") == true ->
showError("The server is temporarily unavailable. Please try again later.")
else ->
showError("Something went wrong: ${e.message}")
}
}

Data Retrieval Errors

Data retrieval APIs throw exceptions on failure. They do not return AFResult:

lifecycleScope.launch {
try {
val profile = AFCore.profile().get()
// Use the profile
displayProfile(profile)
} catch (e: AFException) {
when {
e.message?.contains("Network error") == true -> {
// Show offline state, offer retry
showOfflineState()
}
e.detail != null -> {
// Server returned structured error details
Log.e("Profile", "Detail: ${e.detail}")
Log.e("Profile", "Errors: ${e.errors}")
showError(e.message ?: "Failed to load profile")
}
else -> {
showError(e.message ?: "Failed to load profile")
}
}
}
}

Data Mutation Errors

Mutation operations (POST, PUT, DELETE) also throw exceptions on failure:

lifecycleScope.launch {
try {
AFCore.barcode().submitBarcodeId(id = scannedValue, lat = null, lng = null)
showSuccess("Check-in successful!")
} catch (e: AFException) {
if (e.errors?.isNotEmpty() == true) {
// Server returned validation errors
val errorMessages = e.errors!!.filterNotNull().joinToString("\n")
showError("Validation failed:\n$errorMessages")
} else {
showError(e.message ?: "Submission failed")
}
}
}

Token Errors and Automatic Recovery

Token-related errors are handled automatically by the SDK. Consumer apps do not need to catch 401 errors or manage token refresh manually:

ScenarioSDK BehaviorApp Action Needed
Access token expiredAuth plugin triggers refresh automaticallyNone
Refresh token expiredRe-auth attempted with stored external IDNone (if re-auth succeeds)
Re-auth also failsTokens cleared, SESSION_EXPIRED emittedHandle SESSION_EXPIRED in session observer
Rate limited (429)Exponential backoff, retry laterNone
Server error (5xx) during refreshExponential backoff, use existing tokenNone

See Session Management for how to handle SESSION_EXPIRED.

Common Error Scenarios

Network Connectivity

AFException(message = "Network error. Please try again later.", isError = true)

This occurs when:

  • The device has no internet connection.
  • DNS resolution fails.
  • The connection is refused.
  • A timeout occurs (connect: 15s, socket: 30s, request: 30s).

Recommended handling: Show an offline indicator, offer a retry button.

Invalid Response Format

AFException(message = "Invalid or unexpected response format", isError = true)

This occurs when:

  • The server returns HTML instead of JSON.
  • The response body cannot be deserialized to the expected type.
  • A proxy or firewall intercepts the request.

Recommended handling: Log the error with e.detail for debugging. Show a generic error to the user.

Client Errors (4xx)

AFException(
message = "Resource not found",
isError = true,
detail = "The requested facility does not exist",
errors = ["facilityId: invalid"]
)

These are parsed from the server's error response body and contain structured information:

  • message: Primary error message from the server.
  • detail: Additional context.
  • errors: List of specific validation or field-level errors.

Recommended handling: Display message to the user. Log detail and errors for debugging.

Server Errors (5xx)

AFException(
message = "Internal server error",
isError = true,
detail = "An unexpected error occurred on the server"
)

Recommended handling: Show a "try again later" message. These are typically transient.

Uninitialized SDK

IllegalStateException("AFCore has not been initialized. Call AFCore.initialize() first.")

This is thrown by all feature accessors if AFCore.initialize() has not been called. This is a programming error, not a runtime error.

Recommended handling: Ensure initialize() is called before any SDK usage. See Initialization.

Structured Error Handling Example

Here is a comprehensive error handling pattern that covers all scenarios:

sealed class UiError {
data class Network(val retry: suspend () -> Unit) : UiError()
data class Server(val message: String) : UiError()
data class Validation(val errors: List<String>) : UiError()
data class Generic(val message: String) : UiError()
}

suspend fun <T> withErrorHandling(
onError: (UiError) -> Unit,
block: suspend () -> T
): T? {
return try {
block()
} catch (e: AFException) {
val uiError = when {
e.message?.contains("Network error") == true ->
UiError.Network(retry = { withErrorHandling(onError, block) })

e.errors?.filterNotNull()?.isNotEmpty() == true ->
UiError.Validation(e.errors!!.filterNotNull())

e.isError == true && e.detail != null ->
UiError.Server(e.detail!!)

else ->
UiError.Generic(e.message ?: "An error occurred")
}
onError(uiError)
null
}
}

// Usage
lifecycleScope.launch {
val profile = withErrorHandling(
onError = { error ->
when (error) {
is UiError.Network -> {
showRetrySnackbar(error.retry)
}
is UiError.Validation -> {
showValidationErrors(error.errors)
}
is UiError.Server -> {
showError(error.message)
}
is UiError.Generic -> {
showError(error.message)
}
}
}
) {
AFCore.profile().get()
}

profile?.let { displayProfile(it) }
}