Health Connect SecurityException: The Permission Was Not Granted
Last verified October 3, 2026 · 5 min read
Sources checked (9) · October 3, 2026
- developer.android.com/health-and-fitness/health-connect/read-data — exception table: SecurityException description and best practice; permissions revoked on app deletion
- developer.android.com/health-and-fitness/health-connect/write-data — same exception table on the write side
- developer.android.com/health-and-fitness/health-connect/get-started — manifest declaration, getGrantedPermissions, createRequestPermissionResultContract, check permissions every time
- developer.android.com/health-and-fitness/health-connect/sync-data — separate changes tokens per data type to avoid an Exception when one permission is revoked
- developer.android.com/reference/kotlin/androidx/health/connect/client/HealthConnectClient — SecurityException under Throws for reads, writes, aggregate, getChanges; PERMISSION_WRITE_MEDICAL_DATA note
- developer.android.com/reference/kotlin/androidx/health/connect/client/PermissionController — getGrantedPermissions and createRequestPermissionResultContract
- developer.android.com/reference/kotlin/androidx/health/connect/client/permission/HealthPermission — PERMISSION_READ_HEALTH_DATA_IN_BACKGROUND: reading in background without it may result in an error
- developer.android.com/reference/android/health/connect/HealthConnectException — ERROR_SECURITY description
- github.com/androidx/androidx/blob/androidx-main/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/ExceptionConverter.kt — Jetpack converts ERROR_SECURITY to SecurityException(this)
Covered here:Health Connect
Your Android app calls readRecords, insertRecords, aggregate or getChanges, and instead of data you get java.lang.SecurityException. Health Connect is not telling you the data is missing. It is telling you the call needed a permission your app does not hold at that moment. Google documents exactly one meaning for it, and almost every fix comes down to checking the granted set before you call.
What Google documents#
Google's read-data and write-data guides carry the same exception table. The SecurityException row reads, verbatim:
There are issues encountered when the requests require permissions that aren't granted.
The recommended best practice in the same row: "make sure that you've declared use of Health Connect data types for your published app. Also, you must declare Health Connect permissions in the manifest file and in your activity." (Both guides fetched October 3, 2026: read data, write data.)
Android's Jetpack HealthConnectClient reference lists SecurityException under Throws for readRecords, readRecord, insertRecords, aggregate, deleteRecords, getChanges and getChangesToken, each time with the same gloss: "For requests with unpermitted access." The one place it names a specific permission is the medical-records API: "Caller must hold PERMISSION_WRITE_MEDICAL_DATA in order to call this API, otherwise a SecurityException will be thrown." Health Connect error codes sets SecurityException beside the other exception types the HealthConnectClient reference documents, with how many methods each appears on.
Where the HealthConnectException in your stack trace comes from#
On Android 14 and later Health Connect is part of the system, and the platform API reports failures as android.health.connect.HealthConnectException with an error code. Android's platform reference describes ERROR_SECURITY as "The caller doesn't have the correct permissions for this call. This error may be considered similar to SecurityException." The Jetpack library's own source (ExceptionConverter.kt, read October 3, 2026) converts that code into SecurityException(this) in its platform code path, which requires API 34. So on those devices the SecurityException you catch carries the platform exception as its cause. That is internal library code, not a documented contract, so read the cause for logging and never branch on its message text.
Most likely causes (ranked)#
The ranking is our judgement. Each cause is tied to what Google documents.
- Declared, never granted, or since revoked. The manifest entry makes a permission requestable. It does not grant it. Google's get-started guide says "users can grant or revoke permissions at any time", so a read that worked yesterday can throw today. Google's read guide adds that if a user deletes your app, "all permissions, including the history permission, are revoked", so a reinstall starts from zero.
- Not declared in the manifest. Google's get-started guide says the permissions in your request set must be "declared in your Android manifest first". Health Connect uses standard
uses-permissiontags, one per type and direction. A typo or a missing write entry fails here. - A changes token that spans a revoked type. Google's sync guide recommends "getting separate tokens per data type instead of getting them in bulk to avoid having an Exception in case one of the permissions is revoked." One token over five types fails as a whole when the user revokes any one of them.
- A background read without the background permission. Android's
HealthPermissionreference says "An attempt to read data in background without this permission may result in an error" forPERMISSION_READ_HEALTH_DATA_IN_BACKGROUND. It does not name the exception type, so confirm with your own logs whether this is what you are seeing. - A published app without its Play Console declaration. Google's table row tells you to declare your Health Connect data types for your published app. If a type works in a local build and fails only for Play users, see the production section of Health Connect returns no data.
The fix: check the granted set, then call#
Google's get-started guide is explicit that "your app needs to check for permissions every time before using them and handle scenarios where permission is lost." Ask permissionController.getGrantedPermissions() first, and keep a catch for the gap between your check and the call.
val stepsRead = HealthPermission.getReadPermission(StepsRecord::class)
suspend fun readStepsOrNull(
client: HealthConnectClient,
start: Instant,
end: Instant
): List<StepsRecord>? {
val granted = client.permissionController.getGrantedPermissions()
if (stepsRead !in granted) return null // not granted: ask, don't call
return try {
client.readRecords(
ReadRecordsRequest(
StepsRecord::class,
timeRangeFilter = TimeRangeFilter.between(start, end)
)
).records
} catch (e: SecurityException) {
null // revoked between the check and the call
}
}
A null here means "we may not look", which is different from an empty list. Keep the two apart in anything that draws charts or streaks.
When the permission is missing, request it with the Health Connect contract, which Google's get-started guide says displays the Health Connect permissions screen. Android's PermissionController reference documents createRequestPermissionResultContract(), and the callback returns the set the user actually granted:
val requestPermissions = registerForActivityResult(
PermissionController.createRequestPermissionResultContract()
) { granted ->
if (stepsRead in granted) {
// safe to read
} else {
// denied: degrade, don't re-prompt in a loop
}
}
requestPermissions.launch(setOf(stepsRead))
And the manifest side, in the format Google's get-started guide shows:
<uses-permission android:name="android.permission.health.READ_STEPS"/>
<uses-permission android:name="android.permission.health.WRITE_STEPS"/>
Symptom to cause#
| What you observe | Likely cause | What to do |
|---|---|---|
| Throws on first use after install | Never granted | Check, then request through the contract |
| Worked, now throws on every call | User revoked it | Re-check every session; offer reconnect |
getChanges throws after a settings change | One token covers a revoked type | One changes token per record type |
| Throws only in a background worker | Background permission missing | Request PERMISSION_READ_HEALTH_DATA_IN_BACKGROUND |
| No exception, just an empty list | Not this error | Read Health Connect returns no data |
Where to go next#
The full setup, including the manifest and permission flow this page assumes, is in the Google Health Connect integration guide. Every android.permission.health.* string by record type is in the Health Connect records reference. For an availability failure rather than a permission one, see SDK_UNAVAILABLE_PROVIDER_UPDATE_REQUIRED.
Frequently asked questions
- Is a Health Connect SecurityException the same as an empty result?
- No. An empty list means the call was allowed and found nothing. A SecurityException means the call was not allowed at all. Google's exception table ties it to requests that need permissions that aren't granted. Keep the two apart in your code: an empty list can be shown as zero, but a SecurityException should lead to a permission check and a reconnect prompt, never a zero.
- Why does getChanges throw SecurityException after the user changes permissions?
- Most likely because one changes token covers several record types and the user revoked one of them. Google's sync guide recommends getting separate tokens per data type instead of getting them in bulk, to avoid an Exception if one of the permissions is revoked. With one token per type, losing one permission stops only that type's sync.
- Is declaring a Health Connect permission in the manifest enough?
- No. The manifest entry is what makes a permission requestable, and Google's get-started guide says every permission in your request set must be declared there first. Granting happens at runtime through the Health Connect permissions screen, which you open with PermissionController.createRequestPermissionResultContract. The user can also revoke a grant at any time, so check getGrantedPermissions before each use.
- What is the HealthConnectException in my SecurityException stack trace?
- On Android 14 and later the platform reports the failure as android.health.connect.HealthConnectException with the code ERROR_SECURITY, which Android describes as the caller lacking the correct permissions. The Jetpack library's source wraps that in a SecurityException, so the platform exception shows up as the cause. That wrapping is internal library code, so log the cause but do not branch on its message.
Keep reading
Elsewhere on the site
Pages that share this one’s concepts and sources, from other sections.
Next steps
Was this page useful?
Independent comparison, last reviewed October 3, 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 troubleshooting · by AIFitnessAPI