In React Native, why can Linking.canOpenURL for another app's custom scheme return false or reject even though that app is installed?
answer
- querying other apps is restricted
- iOS: LSApplicationQueriesSchemes
- unlisted scheme on iOS: rejects
- Android 11+: <queries> in the manifest
- openURL does not call canOpenURL
basics
~20 sBoth platforms restrict which apps yours may query. On iOS, a custom scheme missing from LSApplicationQueriesSchemes makes React Native's canOpenURL reject; on Android 11+, a scheme not declared in the manifest's queries element makes it resolve false.
solid answer
~50 s`canOpenURL` asks the OS whether some app handles a URL, and both platforms limit that query for privacy. On iOS, the scheme must be listed in `LSApplicationQueriesSchemes` in `Info.plist`; React Native's iOS implementation rejects with "Unable to open URL … Add <scheme> to LSApplicationQueriesSchemes" when a custom scheme is not listed, and resolves `false` when it is listed but no app handles it. On Android, when targeting Android 11 (API 30) or later, package visibility applies: without a `<queries>` entry for a `VIEW` intent with that scheme, the lookup finds nothing and React Native resolves `false`. In Expo, the iOS list goes under `ios.infoPlist` and the Android `<queries>` needs a config plugin. Often the simpler design is to skip the check: `openURL` does not call `canOpenURL`, so try it and handle the rejection with a store or web fallback.
code
tsx · 20 linesimport { Linking } from 'react-native';
const AUTH_APP_URL = 'authapp://verify';
const AUTH_APP_WEB_URL = 'https://auth.example.com/verify';
export async function openAuthenticator(): Promise<void> {
try {
await Linking.openURL(AUTH_APP_URL);
} catch {
await Linking.openURL(AUTH_APP_WEB_URL);
}
}
export async function isAuthenticatorVisible(): Promise<boolean> {
try {
return await Linking.canOpenURL(AUTH_APP_URL);
} catch {
return false;
}
}go deeper
Remember that canOpenURL can say false for an installed app when the app has not declared the scheme it wants to query.
Name LSApplicationQueriesSchemes and the Android 11 queries element, and the different React Native outcomes on each platform.
Prefer openURL with a fallback where possible, wrap canOpenURL for rejections, and configure both platforms in bare and Expo projects.
Weigh the privacy-driven query limits against product needs: which installed-app checks are worth declaring and maintaining at all.
## What canOpenURL asks `Linking.canOpenURL(url)` asks the operating system whether **any installed app** can handle a URL, and returns a `Promise<boolean>`. The classic use is a password-reset app that offers "Open in your authenticator app" only when that app is installed. The trouble is that knowing which apps are installed is sensitive, so both platforms restrict the query, and an unconfigured app gets a misleading answer. ## iOS: LSApplicationQueriesSchemes Since iOS 9, an app may only query schemes it has declared in **`LSApplicationQueriesSchemes`**, an array in `Info.plist`. React Native's iOS implementation distinguishes three cases for a custom scheme: | Situation | React Native `canOpenURL` result | |---|---| | An app handles the scheme, and it is declared | resolves `true` | | Declared, but no app handles it | resolves `false` | | **Not declared** in `LSApplicationQueriesSchemes` | **rejects**: "Unable to open URL: … Add <scheme> to LSApplicationQueriesSchemes in your Info.plist." | For `http` and `https` URLs no declaration is needed; they resolve `false` only if nothing can open them. React Native's documentation phrases the undeclared case both as "rejects" and as "resolves `false`" in different paragraphs; the 0.87 source rejects, so code must handle both outcomes. ## Android: package visibility and `<queries>` On Android, React Native builds a `VIEW` intent for the URL and asks the package manager whether any activity resolves it; the result is `true` or `false`. When the app **targets Android 11 (API level 30) or later**, the package manager only reveals apps your manifest has declared interest in. Without a matching entry, the lookup finds nothing and `canOpenURL` resolves **`false`**, even though the target app is installed. The fix is a `<queries>` element in `AndroidManifest.xml`: ```xml <queries> <intent> <action android:name="android.intent.action.VIEW" /> <data android:scheme="authapp" /> </intent> </queries> ``` React Native's docs describe the Android failure as a rejection; the 0.87 implementation resolves `false` and rejects only if the check itself throws. ## In an Expo project - iOS: set `ios.infoPlist.LSApplicationQueriesSchemes` in the app config, for example `["authapp"]`. - Android: add the `<queries>` element with a config plugin that modifies the Android manifest; Expo's linking guide shows one. - Both are native changes: rebuild, and test in a development build rather than Expo Go. ## openURL does not need canOpenURL `Linking.openURL` does not call `canOpenURL`. On Android it starts the `VIEW` intent and rejects with "Could not open URL" if nothing can handle it; on iOS it asks the system to open the URL and rejects with "Unable to open URL" if that fails. So for "open the authenticator app, otherwise go to its store page", a common design is: 1. `try { await Linking.openURL('authapp://…') }` 2. `catch { await Linking.openURL(storeOrWebUrl) }` This needs no query declarations, because you never ask what is installed. Keep `canOpenURL` for UI that must know *before* the tap, such as hiding a button — and then declare the scheme on both platforms. ## Debugging checklist - iOS promise rejected with the `LSApplicationQueriesSchemes` message → add the scheme and rebuild. - Android returns `false` on 11+ but `true` on older devices → add `<queries>`. - Works in a development build but not after a fresh prebuild → the Expo config plugin for `<queries>` is missing from `plugins`. - Scheme typed with uppercase letters → React Native's iOS check compares the scheme as written and in lowercase; declare it in lowercase. ## What a strong answer shows It knows both platform restrictions by name, the exact React Native outcomes (reject on iOS for an undeclared scheme, `false` on Android), how to configure each in bare and Expo projects, and when to skip the check entirely in favour of `openURL` with a fallback.
- If canOpenURL is unreliable without configuration, why not always use it before openURL?Because it adds configuration without adding safety. `openURL` already rejects when nothing handles the URL, so `try`/`catch` with a fallback covers the missing-app case with no query declarations. `canOpenURL` earns its keep only when the UI must know in advance, for example to hide a button.
- Does declaring a scheme in LSApplicationQueriesSchemes let your app open it?Opening never needed it; the key only permits querying with `canOpenURL`. What it changes in React Native is the answer for that scheme: a real `true` or `false` instead of a rejection.
saying these in an interview costs you the question
- canOpenURL returning false proves the other app is not installed.
- LSApplicationQueriesSchemes is required before openURL can open a custom scheme.
- On Android, canOpenURL works the same on every API level without manifest changes.
- openURL internally calls canOpenURL, so both need the same declarations.
- In React Native on iOS, an undeclared custom scheme always resolves false and never rejects.