Recipe: A Health Connect Error Policy: Retry, Ask, Fix or Report
Last verified November 5, 2026 · 8 min read
Covered here:Health ConnectApple HealthKit
Health Connect reports failures two ways. The platform API (android.health.connect, API level 34 and up) throws a HealthConnectException carrying an error code; the Jetpack HealthConnectClient throws ordinary exceptions such as SecurityException and RemoteException. Either way the caller has the same decision to make, and Google's reference states enough to make it. The site's Health Connect error codes page holds the generated copy, read October 3, 2026; this recipe turns it into a policy.
The nine codes and what to do#
| Code | Google's sentence | Action |
|---|---|---|
ERROR_UNKNOWN (1) | "An unknown error occurred while processing the call." | report |
ERROR_INTERNAL (2) | "An internal error occurred which the caller cannot address." | report |
ERROR_INVALID_ARGUMENT (3) | "The caller supplied invalid arguments to the call." | fix the request |
ERROR_IO (4) | "The call might succeed if repeated." | retry later |
ERROR_SECURITY (5) | "The caller doesn't have the correct permissions for this call." | ask the user |
ERROR_REMOTE (6) | "An IPC related error occurred." | retry later |
ERROR_RATE_LIMIT_EXCEEDED (7) | "Caller should try this api call again later." | retry later |
ERROR_DATA_SYNC_IN_PROGRESS (8) | "Caller may try this api call again later." | retry later |
ERROR_UNSUPPORTED_OPERATION (9) | "This operation is currently not supported by the platform." | report |
The actions are this site's reading of those sentences; Google does not publish a retry table. Where the reference offers no recovery, the action is report, the one that does not pretend to know.
What the recipe does#
- Resolves anything a failure looks like: a code name, a numeric value, an error carrying
errorCode, or an exception by simple or qualified class name. - Returns the evidence with every decision, so a log line says why a call was or was not retried.
- Knows the one method-dependent rule. A
RemoteExceptionfromdeleteRecords,updateRecordsorreadRecordis classified fix-request, because Google says invalid ids surface there as IPC failures. - Wraps a call with capped exponential backoff for retryable failures only, rethrowing everything else on the first attempt with
.policyattached.
The implementation#
The canonical file is cookbook/health-connect-error-policy.mjs in this site's repository, and cookbook/health-connect-error-policy.test.mjs runs against it on every CI run. The listing below is a verbatim copy of that file. It has no runtime dependencies and needs Node 20 or newer; the two policy tables are plain data, so they port to Kotlin line for line.
/**
* health-connect-error-policy.mjs — decide what to do with a Health Connect
* failure: retry later, ask the user, fix the code, or stop and report.
*
* WHAT THIS IMPLEMENTS
* Android's platform API reports failures as HealthConnectException error
* codes; the Jetpack HealthConnectClient throws ordinary exceptions. This
* file maps both to one of four actions, and keeps beside each mapping the
* sentence in Google's reference that justifies it:
* RETRY_LATER Google says the call might succeed if repeated, or that
* the caller should (or may) try again later
* ASK_USER a permission is missing: only the user can fix that
* FIX_REQUEST the caller sent something invalid; retrying sends it again
* REPORT Google gives no recovery the caller can perform
* The mapping is this site's reading of those sentences, not a table
* Google publishes. Where the reference is silent (ERROR_UNKNOWN), the
* action is REPORT, the one that does not pretend to know.
*
* One rule depends on the method. HealthConnectClient documents that
* deleting by invalid identifiers, and updating with invalid identifiers,
* "will result in IPC failure": so a RemoteException from deleteRecords,
* updateRecords or readRecord can be your bad id, not a transport hiccup,
* and it is not retried blindly.
*
* `policyFor(error, { method })` accepts an error code name ("ERROR_IO"),
* its value (4), or a thrown error whose name is the exception class. It
* returns `{ action, retryable, reason, evidence }`.
*
* WHERE THE SENTENCES COME FROM
* https://aifitnessapi.com/error-codes/health-connect — the site's
* generated copy (src/data/errorCodes.ts) of Android's HealthConnectException
* reference and Jetpack's HealthConnectClient Throws tables, read
* 2026-10-03. The test checks every sentence below against that file and
* fails if Google adds an error code this policy does not cover.
*
* No dependencies. Node 20+ (the policy table is plain data, so it ports to
* Kotlin line for line).
*
* MIT — from aifitnessapi.com/cookbook
*/
export const ACTION = Object.freeze({
RETRY_LATER: "retry-later",
ASK_USER: "ask-user",
FIX_REQUEST: "fix-request",
REPORT: "report",
});
/** HealthConnectException codes (android.health.connect, API level 34+). */
export const CODE_POLICY = Object.freeze({
ERROR_UNKNOWN: { value: 1, action: ACTION.REPORT, evidence: "An unknown error occurred while processing the call." },
ERROR_INTERNAL: { value: 2, action: ACTION.REPORT, evidence: "An internal error occurred which the caller cannot address." },
ERROR_INVALID_ARGUMENT: { value: 3, action: ACTION.FIX_REQUEST, evidence: "The caller supplied invalid arguments to the call." },
ERROR_IO: { value: 4, action: ACTION.RETRY_LATER, evidence: "An issue occurred reading or writing to storage. The call might succeed if repeated." },
ERROR_SECURITY: { value: 5, action: ACTION.ASK_USER, evidence: "The caller doesn't have the correct permissions for this call." },
ERROR_REMOTE: { value: 6, action: ACTION.RETRY_LATER, evidence: "An IPC related error occurred." },
ERROR_RATE_LIMIT_EXCEEDED: { value: 7, action: ACTION.RETRY_LATER, evidence: "Caller should try this api call again later." },
ERROR_DATA_SYNC_IN_PROGRESS: { value: 8, action: ACTION.RETRY_LATER, evidence: "Caller may try this api call again later." },
ERROR_UNSUPPORTED_OPERATION: { value: 9, action: ACTION.REPORT, evidence: "This operation is currently not supported by the platform." },
});
/** Jetpack HealthConnectClient exceptions, by simple class name. */
export const EXCEPTION_POLICY = Object.freeze({
SecurityException: { action: ACTION.ASK_USER, evidence: "For requests with unpermitted access." },
RemoteException: { action: ACTION.RETRY_LATER, evidence: "For any IPC transportation failures." },
IOException: { action: ACTION.RETRY_LATER, evidence: "For any disk I/O issues." },
IllegalArgumentException: { action: ACTION.FIX_REQUEST, evidence: "if the size of ids is too large or any ID is deemed as invalid." },
IllegalStateException: { action: ACTION.REPORT, evidence: "If service is not available." },
UnsupportedOperationException: { action: ACTION.REPORT, evidence: "if the feature is not available." },
});
/** Methods where Google says invalid identifiers surface as an IPC failure. */
export const ID_SENSITIVE_METHODS = Object.freeze({
deleteRecords: "Deleting by invalid identifiers such as a non-existing identifier or deleting the same record multiple times will result in IPC failure.",
updateRecords: "Update with invalid identifiers will result in IPC failure.",
readRecord: "Update with invalid identifiers will result in IPC failure.",
});
const BY_VALUE = new Map(Object.entries(CODE_POLICY).map(([name, p]) => [p.value, name]));
function shape(kind, name, p, extra = {}) {
return { kind, name, action: p.action, retryable: p.action === ACTION.RETRY_LATER, evidence: p.evidence, ...extra };
}
/**
* @param {string|number|{ name?: string, errorCode?: number }} error
* @param {{ method?: string }} [ctx]
*/
export function policyFor(error, { method } = {}) {
if (typeof error === "number" || (error && typeof error.errorCode === "number")) {
const value = typeof error === "number" ? error : error.errorCode;
const name = BY_VALUE.get(value);
if (!name) return { kind: "code", name: null, action: ACTION.REPORT, retryable: false, evidence: null, reason: `unknown HealthConnectException code ${value}` };
return policyFor(name, { method });
}
const name = typeof error === "string" ? error : error?.name;
if (name && CODE_POLICY[name]) return shape("code", name, CODE_POLICY[name]);
const simple = name?.split(".").pop();
if (simple && EXCEPTION_POLICY[simple]) {
const p = EXCEPTION_POLICY[simple];
if (simple === "RemoteException" && method && ID_SENSITIVE_METHODS[method]) {
return shape("exception", simple, { action: ACTION.FIX_REQUEST, evidence: ID_SENSITIVE_METHODS[method] }, { reason: `${method} reports invalid ids as an IPC failure: check the ids before retrying` });
}
return shape("exception", simple, p);
}
return { kind: "unknown", name: name ?? null, action: ACTION.REPORT, retryable: false, evidence: null, reason: "not a Health Connect error this policy knows" };
}
/**
* Run `fn`, retrying only what the policy calls retryable, with capped
* exponential backoff. Everything else is rethrown with `.policy` attached so
* the caller can route ASK_USER to a permission screen.
*/
export async function withHealthConnectPolicy(fn, { method, attempts = 4, baseMs = 1000, maxMs = 60_000, sleep = (ms) => new Promise((r) => setTimeout(r, ms)) } = {}) {
for (let attempt = 0; ; attempt++) {
try {
return await fn();
} catch (err) {
const policy = policyFor(err, { method });
if (!policy.retryable || attempt + 1 >= attempts) {
if (err && typeof err === "object") err.policy = policy;
throw err;
}
await sleep(Math.min(maxMs, baseMs * 2 ** attempt));
}
}
}
How to adapt it#
Port the tables, keep the evidence. In a Kotlin app, a when over HealthConnectException.errorCode and over the exception class reproduces this; keep the sentence as a comment so the next reviewer can check it.
Route ask-user to one screen. Every SecurityException path should end at the same permission request, which the SecurityException fix walks through.
Give rate limits room. For ERROR_RATE_LIMIT_EXCEEDED, a longer base delay is reasonable; the Health Connect rate-limit fix covers what Google documents about quotas.
The contract the tests hold it to#
Against src/data/errorCodes.ts: every generated ERROR_* constant has a policy with the same value and the policy lists no code the reference lacks; every code's evidence and every exception's and method's evidence appears verbatim in the generated reference; exactly the four codes above are retryable; codes resolve by value and from errorCode; exceptions resolve by simple and qualified name; a RemoteException from the three id-sensitive methods is not retried; the wrapper retries a rate limit with 1 s then 2 s backoff, never retries a permission error, and gives up after its attempt budget.
What it deliberately does not do#
It does not cover HealthKit's HKError.Code (see HealthKit errors), it does not request permissions itself, and it does not handle an expired changes token, which the incremental sync recipe does.
Frequently asked questions
Which Health Connect error codes are safe to retry?
Four, by Google's own wording as the site read it on October 3, 2026: ERROR_IO, which the reference says might succeed if repeated; ERROR_RATE_LIMIT_EXCEEDED, where the caller should try this api call again later; ERROR_DATA_SYNC_IN_PROGRESS, where the caller may try again later; and ERROR_REMOTE, an IPC error the reference likens to RemoteException. The other five are not fixed by waiting: ERROR_SECURITY needs the user to grant a permission, ERROR_INVALID_ARGUMENT needs a different request, and ERROR_UNKNOWN, ERROR_INTERNAL and ERROR_UNSUPPORTED_OPERATION are reported. That split is this site's reading of Google's sentences, not a table Google publishes.
Why should a RemoteException from Health Connect deleteRecords not be retried?
Because Google documents it as a likely symptom of your own input. HealthConnectClient's Throws table for deleteRecords says that deleting by invalid identifiers, such as a non-existing identifier or deleting the same record multiple times, will result in IPC failure, and for updateRecords and readRecord that update with invalid identifiers will result in IPC failure. So on those methods a RemoteException may be your bad id rather than a transport hiccup, and retrying sends the same bad id again. The recipe's policyFor takes the method name and returns fix-request for a RemoteException from those three, while other methods' RemoteExceptions stay retryable.
How should an app respond to a Health Connect SecurityException at runtime?
Route the user to a permission request rather than retrying. Google's HealthConnectClient reference documents SecurityException for requests with unpermitted access across reads, writes, aggregates and changes, and the platform code ERROR_SECURITY means the caller does not have the correct permissions for the call. A permission the user revoked in settings does not come back on its own, so a retry loop only burns rate limit and battery. The recipe classifies both as ask-user, and its retry wrapper rethrows them on the first attempt with the policy attached, so the caller can show the permission screen.
Keep reading
Elsewhere on the site
Pages that share this one’s concepts and sources, from other sections.
- TroubleshootingHealth Connect changesTokenExpired: How to Resync Without Losing Data
- Health Connect APIREAD_HEALTH_DATA_IN_BACKGROUND: reading Health Connect data from a background worker
- ArchitectureIncremental Sync: Reading Only What Changed Since Last Time
- ComplianceWhat does Google Play's health data policy require?
Next steps
Was this page useful?
Corrections that change a page are logged, and credited by name if you ask: contributors.
Cite this page
- Title
- Recipe: A Health Connect Error Policy: Retry, Ask, Fix or Report
- Publisher
- AIFitnessAPI
- URL
- https://aifitnessapi.com/cookbook/health-connect-error-policy
- Last verified
AIFitnessAPI. "Recipe: A Health Connect Error Policy: Retry, Ask, Fix or Report." Last verified 2026-11-05. https://aifitnessapi.com/cookbook/health-connect-error-policy
@misc{aifitnessapi-cookbook-health-connect-error-policy,
author = {{AIFitnessAPI}},
title = {Recipe: A Health Connect Error Policy: Retry, Ask, Fix or Report},
year = {2026},
url = {https://aifitnessapi.com/cookbook/health-connect-error-policy},
urldate = {2026-11-05},
note = {Last verified 2026-11-05}
}Cite the page, not the site — every page carries its own verification date.
Independent comparison, last reviewed November 5, 2026. Pricing, rate limits, and feature availability change often — confirm current details in each provider’s official documentation before you commit. Product and company names are trademarks of their respective owners; AIFitnessAPI is not affiliated with, endorsed by, or sponsored by any product listed here.
← All cookbook · by AIFitnessAPI