Skip to content
AF
healthkit
6 min readAIFitnessAPI

'No Data' Means Four Different Things

An empty health read has several causes and only one is 'connect your device'. Here is the order to rule them out before you write that empty state.

healthkithealth-connectarchitecturedata

When a health query comes back empty, your app has learned almost nothing. Four different situations produce the identical result, and the empty state most apps ship addresses exactly one of them.

CauseWhat your code observesCan you detect it?Who can fix it
The user denied the readNo error, empty resultNoThe user, in the Health app
Permission granted, nothing recordedNo error, empty resultNoNobody. There is no data yet
Data exists, outside your windowNo error, empty resultYes, by widening the queryYou
Store unavailable or restrictedAn error you can inspectYesDepends on the case

Only the last row announces itself. The other three are the same silence, and telling them apart is the whole job.

Why the first two are indistinguishable#

Apple documents the authorization-denied error as the case where the user has not given the app permission to save data. There is no read-denied case in the 17 errors HealthKit defines. A refused read completes normally and hands you an empty array — identical to a query against a type the user authorised and simply has no samples for. That design decision, and the full error list, are covered in the HealthKit error that never fires.

Accept it as a permanent constraint rather than a debugging challenge. There is no probe, no side channel, no status API that separates them. Every hour spent trying to distinguish denial from absence is an hour not spent making your UI correct under both.

The diagnostic order#

Work down this list. Each step is cheaper and more conclusive than the one below it, and stopping early is how teams end up blaming permissions for a timezone bug.

1. Is the store even available? These are the errors that fire, and each means something different:

  • errorHealthDataUnavailable — unsupported device. Hide the feature entirely. Do not retry, do not prompt, do not show an onboarding card.
  • errorHealthDataRestricted — a Mobile Device Management profile restricts HealthKit on this device. The user cannot fix this and neither can you. Say so plainly; a corporate device is a real and permanent state.
  • errorDatabaseInaccessible — the data is protected and the device is locked. This is transient. Retry after unlock. Rendering it as an error is a bug, and it is the classic failure mode of a background refresh that runs before first unlock.

The full list is at /healthkit-errors. On the Android side the error surface is different in shape but the reasoning is the same; start from health-connect-no-data.

2. Did you actually ask? errorAuthorizationNotDetermined means the authorization request never ran for that type. This is not a user problem, it is a launch-sequence bug: a type added to your read set months after the request code was written, a request that ran before the store was ready, or a type requested on one code path and read on another. Check it before you check anything else about the user.

3. Widen the window, deliberately absurdly. Before drawing any conclusion, re-run the same query with no meaningful date bound. If the all-time query returns samples and yours does not, your problem is the window, and no amount of permission debugging will help. This single check resolves more "no data" reports than everything else on this list.

4. Check the day boundary and the timezone. A query for "today" is a query for an interval, and which interval depends on whose midnight you used. Samples written while a user was travelling land on a different local day than the one they expect. A rollup computed in UTC against a user in a distant offset drops or duplicates the edges. This has its own failure taxonomy in timezones and day boundaries, and the arithmetic is worked through at /day-boundaries.

5. Now, and only now, accept the ambiguity. What is left is a denied read or a genuinely empty store, and you cannot tell which. Design for the union.

The empty state that lies#

Here is the copy nearly every health app ships:

Connect your Apple Watch to see your data.

Consider who sees it. The user who owns a watch, granted every permission, and did not wear it yesterday — now told to connect a device that is connected. The user who deliberately denied step sharing — now told there is a hardware problem, when the real state is a choice they made and can revisit. The user whose data is fine but whose timezone broke your rollup — sent to a settings screen that will not help. In none of those cases is the instruction true, and in two of them it actively misdirects.

An empty state built on an assumption you cannot verify is a guess rendered as a fact. Write copy that is true under all remaining causes:

We didn't find any step data for the last week. If you've turned off sharing for steps, you can change that in the Health app. You can also try a longer date range.

It states the observation, offers both branches, and asserts neither. It is less confident and more useful. Product-side guidance for the platform-specific version of this screen is in healthkit-no-data.

Instrument the ambiguity instead of guessing at it#

You cannot resolve the last two cases for an individual user, but you can measure them in aggregate, and the aggregate is actionable. Log which branch every empty read took:

BucketWhat it meansWhat to do when it grows
Never askedRequest-flow bugFix immediately; this is entirely yours
Store unavailable or restrictedDevice or MDMSegment these users out of your funnel
Empty in window, non-empty all-timeWindow or day-boundary bugFix the query, not the copy
Empty all-timeDenial or genuine absenceWatch the trend against your consent screen

That last bucket is the one you cannot decompose, and it is also the one worth watching over time. If it grows after you change your permission prompt, you learned something about your prompt. If it grows after you add a read type, you learned something about that type. Wire it into your missing data and gaps handling rather than into a crash reporter, because none of this is a crash.

Do this#

  1. Add the four buckets above to your telemetry before you touch any copy.
  2. Put the deliberately-wide diagnostic query behind a debug flag so support can run it on a real account instead of guessing.
  3. Rewrite every empty state that names a device or a cable. Replace it with the observation plus both branches.
  4. Handle errorDatabaseInaccessible as a retry, never as a failure.
  5. Treat the residual ambiguity as permanent, and stop paying engineers to attack it.

Frequently asked questions

Why is HealthKit returning no data even though the user granted permission?
The most common causes are a query window that does not overlap the samples, a day boundary computed in the wrong timezone, or a store that genuinely holds nothing because no device has written that type. Rule those out in order before concluding anything about permissions, because permission problems and genuine absence look identical from inside a query.
How can I detect that a user denied HealthKit read access?
You cannot detect it from the read itself. Apple documents the authorization-denied error for saving data, so a refused read returns no error and an empty result, indistinguishable from a store with nothing in it. Build one empty state that is honest under both readings rather than guessing which one applies.
What should my empty state say when a health query returns nothing?
Say what you looked for and over what period, then offer both branches without asserting either: a way to widen the range, and a way to check sharing settings. Do not tell the user to connect a device you have no evidence is disconnected. Every unnecessary instruction sends someone to fix something that is not broken.

Read next

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