Skip to content
AF
healthkit
6 min readAIFitnessAPI

The HealthKit Error That Never Fires

HealthKit defines 17 error cases. The authorization-denied one is documented for saving, not reading — which is why an empty read is so hard to debug.

healthkitiosapi

Apple's HKError type defines 17 cases. The one every iOS developer expects to handle — the user said no to my read — is not among them in the form they assume.

Here is Apple's own wording for the case that looks like it:

errorAuthorizationDenied — The user hasn't given the app permission to save data.

Save. Not read. A read the user refused does not produce that error, or any other. The query completes, the completion handler fires with a nil error, and the sample array is empty. Identical, byte for byte, to a query against a type the user has cheerfully authorised and never generated a single sample for.

That is not a bug and it is not an oversight. It is the single design decision that makes health engineering harder than the rest of your app, and it is worth understanding before you write another empty state.

All 17 cases, as Apple documents them#

Read for this site's dataset on 2026-08-28. The full reference lives at /healthkit-errors; blank cells are blank in Apple's documentation.

CaseApple's description
noErrorNo error occurred.
errorHealthDataUnavailableThe user accessed HealthKit on an unsupported device.
errorHealthDataRestrictedA Mobile Device Management (MDM) profile restricts the use of HealthKit on this device.
errorInvalidArgumentThe app passed an invalid argument to the HealthKit API.
errorAuthorizationDeniedThe user hasn't given the app permission to save data.
errorAuthorizationNotDeterminedThe app hasn't yet asked the user for the authorization required to complete the task.
errorRequiredAuthorizationDeniedThe user hasn't granted the application authorization to access all the required clinical record types.
errorDatabaseInaccessibleThe HealthKit data is unavailable because it's protected and the device is locked.
errorUserCanceledThe user canceled the operation.
errorAnotherWorkoutSessionStartedAnother app started a workout session.
errorUserExitedWorkoutSessionThe user exited your application while a workout session was running.
errorNoDataData is unavailable for the requested query and predicate.
errorBackgroundWorkoutSessionNotAllowed
errorDataSizeExceeded
errorNotPermissibleForGuestUserModeThe app attempted to write HealthKit data while in a Guest User session in visionOS.
errorWorkoutActivityNotAllowed
unknownError

Scan the descriptions for the word read. It is not there. Every authorization-shaped case in the list is about writing, about clinical records, or about not having asked yet.

The three cases people confuse with a denied read#

errorAuthorizationDenied is a write error. If you attempt to save a sample for a type the user refused to share with you, this is what you get. It says nothing about your read permissions, and treating it as a general permission signal is the most common way teams convince themselves they have solved authorization when they have not.

errorAuthorizationNotDetermined means you never asked. If you see this in production you have a bug in your request flow, not a user who declined. It is the one authorization error that is unambiguously yours to fix.

errorNoData — "Data is unavailable for the requested query and predicate" — is the closest thing to a read signal in the list, and it does not separate the cases either. A predicate matching nothing produces it whether the reason is an empty store, a badly chosen window, or a permission you never got.

Why the design is right, even though it hurts#

Consider the alternative. If a denied read returned a distinct error, then any app could learn which health types a user is unwilling to share. Refusing to share a pregnancy-related type, a mental health type, or a blood glucose type would itself be a disclosure. The user would have no way to decline quietly.

An empty result for a refused read means the only thing your app can observe is absence, and absence is what the store looks like for the majority of types and users anyway. The refusal hides in the noise. That is the point.

The practical consequence is that your permission model and your data model collapse into one state. You do not get to ask "am I allowed?" and then "is there anything?" — you get one query and one answer that covers both.

What actually fires, grouped by what you should do about it#

Handling 17 cases individually is busywork. Handle them in these groups.

GroupCasesYour response
Store unavailableerrorHealthDataUnavailable, errorHealthDataRestrictedHide the feature. Do not retry, do not prompt.
Temporarily inaccessibleerrorDatabaseInaccessibleRetry after unlock. Never surface as a failure.
Your bugerrorInvalidArgument, errorAuthorizationNotDetermined, errorDataSizeExceededAlert your team, not the user.
Write refusederrorAuthorizationDenied, errorNotPermissibleForGuestUserModeStop writing; keep reading.
Clinical recordserrorRequiredAuthorizationDeniedSeparate consent flow entirely.
Workout sessionerrorAnotherWorkoutSessionStarted, errorUserExitedWorkoutSession, errorBackgroundWorkoutSessionNotAllowed, errorWorkoutActivityNotAllowedSession lifecycle, not data access.
User actionerrorUserCanceledNot an error. Do not log it as one.
Empty or unknownerrorNoData, unknownErrorDiagnose; never render as "connect your device".

Notice that no group is called "read denied", because there is no case for it.

The probe that does not work#

The obvious trick, once you know errorAuthorizationDenied fires on save, is to write a throwaway sample and see whether it errors. Teams do this. It tells you about your write authorization for that type, and nothing whatsoever about your read authorization, because HealthKit tracks the two separately. You will have written junk into a user's health record in exchange for an answer to a question you did not ask.

Do not do it. There is no probe. Design for the ambiguity instead.

What to do instead#

  1. Log the raw HKError code on every failure, not a stringified message. The cases Apple ships with no description at all are exactly the ones you will need the numeric code to identify later. Feed them into your data-quality monitoring rather than a crash reporter.
  2. Never write copy that assumes empty means denied. The empty state has more than one cause and only one of them is fixable by the user. The diagnostic order is its own post: what an empty health read actually means.
  3. Ask for the smallest useful set of read types. Since you cannot detect refusal, every extra type on the request screen is a chance for a user to deny something you will then silently never receive. Match the set to what your consent copy actually promises.
  4. Test the denial path deliberately. Denying in the Health app and running your read is the only way to see what your UI does in the state you cannot detect at runtime. Our HealthKit test guide covers building that fixture, and the integration guide covers where the request belongs in your launch sequence.

The error you were looking for does not exist. Build for the one that does: silence.

Frequently asked questions

Does HealthKit return an error when the user denies read access?
No. Apple documents the authorization-denied error as the case where the user has not given the app permission to save data. A read the user refused completes normally and hands you an empty result, with no error to inspect. Your query code cannot distinguish a refusal from a store that genuinely holds nothing for that type.
How many error cases does HKError define?
Apple's documentation, read for this site's dataset on 2026-08-28, lists 17 cases. They cover device and environment problems, programming mistakes, authorization state, workout session lifecycle, user cancellation, and an unknown catch-all. Several carry no description text in Apple's own documentation, so their meaning has to be established by testing rather than by reading.
How do I tell whether a HealthKit read was denied or the data is simply missing?
From the query alone, you cannot. Establish everything you can rule out first: confirm the store is available on the device, confirm you actually requested authorization, then widen the time range and check the day boundary you are using. Whatever remains after that is a refusal or genuine absence, and you must design a single empty state that is true for both.

Read next

Last verified . Figures come from this site’s own published datasets; see how we verify.