Fix: Capacitor Not Working — Build Failing, Plugins Not Loading, or Native Features Not Available
Part of: JavaScript & TypeScript Errors
Quick Answer
How to fix Capacitor issues — project setup with Ionic or standalone, native plugin access, iOS and Android build errors, live reload, deep links, push notifications, and migration from Cordova.
The Problem
A Capacitor plugin call returns an error:
import { Camera, CameraResultType } from '@capacitor/camera';
const photo = await Camera.getPhoto({
resultType: CameraResultType.Base64,
});
// Error: "Camera" plugin is not implemented on webOr the native project doesn’t build:
npx cap sync
npx cap open ioserror: Unable to load contents of file list: '.../Build/Pods-App.xcfilelist'Or cap sync fails with mismatched versions:
[error] @capacitor/[email protected] and @capacitor/[email protected] are incompatible.
Upgrade to matching versions.Or live reload doesn’t connect to the device:
[error] Unable to connect to server at http://localhost:3000Why This Happens
Capacitor wraps a web app inside a native WebView and provides a bridge to native APIs. The integration has several failure points, and most of them surface as the same vague error message regardless of which layer is actually broken.
- Plugins have three implementations: web, iOS, Android — when running in the browser, only the web implementation is available. The Camera, Filesystem, and other device-specific plugins only work on actual devices or simulators. Calling them on the web throws “not implemented” unless the plugin has a web fallback.
- Native projects must be in sync with the web project —
cap synccopies the web build output into the native projects and updates native dependencies. Skipping this after adding a plugin or changing the config results in mismatches. - Version alignment is required —
@capacitor/core,@capacitor/ios,@capacitor/android, and all@capacitor/*plugins must be on the same major version. Mixing v4 and v5 packages causes runtime errors. - Live reload requires the device to reach the dev server — the device needs network access to the machine running the dev server.
localhostdoesn’t work from a physical device — use the machine’s local IP. Firewalls and network isolation often block the connection.
The iOS and Android toolchains add their own layer of failures that have nothing to do with Capacitor itself. On iOS, CocoaPods version mismatches, code-signing certificates, and Apple Silicon (M1/M2/M3) Pod build issues account for the majority of “build failed” reports. On Android, Gradle wrapper drift, SDK platform downloads, and Java version mismatches (JDK 17 vs 21) are the usual culprits. Capacitor’s cap doctor command exists precisely because the same web project can build cleanly on one developer’s machine and fail on another’s due to native toolchain state alone.
A third layer is the JavaScript bundle itself. Capacitor serves the contents of webDir from a capacitor:// (iOS) or https://localhost (Android) scheme inside the WebView. Anything that assumes a specific origin — fetch URLs without a base, OAuth redirects that hard-code http://localhost, service workers — will misbehave inside the WebView even though the web build looks fine in a browser. The fix is usually a config flag or a runtime check using Capacitor.getPlatform(), not a build-system change.
Fix 1: Set Up a Capacitor Project
# Add Capacitor to an existing web project
npm install @capacitor/core @capacitor/cli
npx cap init
# Add platforms
npm install @capacitor/ios @capacitor/android
npx cap add ios
npx cap add android// capacitor.config.ts
import type { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = {
appId: 'com.example.myapp',
appName: 'My App',
webDir: 'dist', // Where your web build output goes (dist, build, out)
server: {
// For live reload during development
// url: 'http://192.168.1.100:3000', // Your machine's local IP
// cleartext: true, // Allow HTTP (not HTTPS)
},
plugins: {
SplashScreen: {
launchAutoHide: true,
launchShowDuration: 2000,
backgroundColor: '#ffffff',
},
StatusBar: {
style: 'dark',
},
Keyboard: {
resize: 'body',
resizeOnFullScreen: true,
},
},
ios: {
contentInset: 'automatic',
scheme: 'My App',
},
android: {
buildOptions: {
keystorePath: undefined,
keystoreAlias: undefined,
},
},
};
export default config;Build and sync workflow:
# 1. Build your web app
npm run build
# 2. Copy web assets + sync native plugins
npx cap sync
# 3. Open in IDE
npx cap open ios # Opens Xcode
npx cap open android # Opens Android Studio
# Or run directly (Capacitor 5+)
npx cap run ios --target="iPhone 15"
npx cap run androidFix 2: Use Plugins Correctly
Install each plugin and sync:
npm install @capacitor/camera @capacitor/filesystem @capacitor/geolocation @capacitor/haptics @capacitor/share @capacitor/local-notifications
npx cap sync// Check platform before calling native-only plugins
import { Capacitor } from '@capacitor/core';
// Platform detection
const isNative = Capacitor.isNativePlatform(); // true on iOS/Android
const platform = Capacitor.getPlatform(); // 'ios', 'android', 'web'
// Camera — with web fallback
import { Camera, CameraResultType, CameraSource } from '@capacitor/camera';
async function takePhoto() {
// Check if the plugin is available
if (!Capacitor.isPluginAvailable('Camera')) {
// Fallback: use file input on web
return useFileInput();
}
// Request permissions first on native
const permissions = await Camera.requestPermissions();
if (permissions.camera !== 'granted') {
throw new Error('Camera permission denied');
}
const photo = await Camera.getPhoto({
resultType: CameraResultType.Uri,
source: CameraSource.Camera,
quality: 90,
width: 1024,
allowEditing: false,
});
return photo.webPath; // Usable in <img src="">
}
// Filesystem — read/write app storage
import { Filesystem, Directory, Encoding } from '@capacitor/filesystem';
async function saveFile(filename: string, data: string) {
await Filesystem.writeFile({
path: filename,
data,
directory: Directory.Documents,
encoding: Encoding.UTF8,
});
}
async function readFile(filename: string) {
const result = await Filesystem.readFile({
path: filename,
directory: Directory.Documents,
encoding: Encoding.UTF8,
});
return result.data;
}
// Geolocation
import { Geolocation } from '@capacitor/geolocation';
async function getCurrentPosition() {
const permissions = await Geolocation.requestPermissions();
if (permissions.location !== 'granted') {
throw new Error('Location permission denied');
}
const position = await Geolocation.getCurrentPosition({
enableHighAccuracy: true,
timeout: 10000,
});
return {
lat: position.coords.latitude,
lng: position.coords.longitude,
};
}
// Share
import { Share } from '@capacitor/share';
async function shareContent() {
await Share.share({
title: 'Check this out',
text: 'Sharing from my app',
url: 'https://example.com',
dialogTitle: 'Share with friends',
});
}Fix 3: Fix iOS Build Errors
Common Xcode build failures and their fixes:
# 1. Pod install failed — missing or outdated CocoaPods
cd ios/App && pod install --repo-update
# If that fails:
sudo gem install cocoapods
pod repo update
pod install
# 2. Clean build folder when Xcode caches cause issues
# In Xcode: Product → Clean Build Folder (Cmd+Shift+K)
# Or from terminal:
cd ios && xcodebuild clean
# 3. Version mismatch — update all Capacitor packages
npm install @capacitor/core@latest @capacitor/ios@latest @capacitor/cli@latest
npx cap sync ios
# 4. Minimum iOS version — update in Xcode or Podfile
# ios/App/Podfile
platform :ios, '14.0' # Capacitor 5 requires iOS 14+
# 5. Signing issues — update Team in Xcode
# Xcode → App target → Signing & Capabilities → TeamiOS permissions — add to Info.plist:
<!-- ios/App/App/Info.plist -->
<!-- Camera -->
<key>NSCameraUsageDescription</key>
<string>We need camera access to take photos</string>
<!-- Photo Library -->
<key>NSPhotoLibraryUsageDescription</key>
<string>We need photo library access to select images</string>
<!-- Location -->
<key>NSLocationWhenInUseUsageDescription</key>
<string>We need your location to show nearby places</string>
<!-- Microphone (for video recording) -->
<key>NSMicrophoneUsageDescription</key>
<string>We need microphone access for video recording</string>Fix 4: Fix Android Build Errors
# 1. Gradle sync failed — update Gradle wrapper
cd android && ./gradlew wrapper --gradle-version=8.4
# 2. SDK version mismatch
# android/variables.gradle
ext {
minSdkVersion = 22 // Capacitor 5 minimum
compileSdkVersion = 34
targetSdkVersion = 34
androidxActivityVersion = '1.8.0'
androidxAppCompatVersion = '1.6.1'
androidxCoordinatorLayoutVersion = '1.2.0'
androidxCoreVersion = '1.12.0'
androidxFragmentVersion = '1.6.2'
coreSplashScreenVersion = '1.0.1'
androidxWebkitVersion = '1.9.0'
junitVersion = '4.13.2'
androidxJunitVersion = '1.1.5'
androidxEspressoCoreVersion = '3.5.1'
}
# 3. Clean and rebuild
cd android && ./gradlew clean
npx cap sync androidAndroid permissions — add to AndroidManifest.xml:
<!-- android/app/src/main/AndroidManifest.xml -->
<manifest>
<!-- Internet (usually already there) -->
<uses-permission android:name="android.permission.INTERNET" />
<!-- Camera -->
<uses-permission android:name="android.permission.CAMERA" />
<!-- Location -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<!-- Storage (Android 12 and below) -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application ...>
</manifest>Fix 5: Live Reload on Device
// capacitor.config.ts — development config
const config: CapacitorConfig = {
appId: 'com.example.myapp',
appName: 'My App',
webDir: 'dist',
server: {
// Use your machine's local IP — NOT localhost
url: 'http://192.168.1.100:5173',
cleartext: true, // Allow HTTP on Android
},
};# Find your machine's local IP
# macOS:
ipconfig getifaddr en0
# Windows:
ipconfig | findstr "IPv4"
# Linux:
hostname -IAutomated live reload setup:
# Run dev server accessible on network
npm run dev -- --host 0.0.0.0
# Then sync and run on device
npx cap sync
npx cap run ios --livereload --external
# --external automatically sets the server URL to your IPAndroid cleartext HTTP — required for live reload:
<!-- android/app/src/main/AndroidManifest.xml -->
<application
android:usesCleartextTraffic="true"
...>Fix 6: Push Notifications
npm install @capacitor/push-notifications
npx cap syncimport { PushNotifications } from '@capacitor/push-notifications';
import { Capacitor } from '@capacitor/core';
async function initPushNotifications() {
if (!Capacitor.isNativePlatform()) return;
// Request permission
const permission = await PushNotifications.requestPermissions();
if (permission.receive !== 'granted') {
console.warn('Push notification permission denied');
return;
}
// Register for push notifications
await PushNotifications.register();
// Get the device token
PushNotifications.addListener('registration', (token) => {
console.log('Push token:', token.value);
// Send token to your backend
fetch('/api/devices', {
method: 'POST',
body: JSON.stringify({ token: token.value, platform: Capacitor.getPlatform() }),
});
});
// Registration error
PushNotifications.addListener('registrationError', (error) => {
console.error('Push registration failed:', error);
});
// Notification received while app is in foreground
PushNotifications.addListener('pushNotificationReceived', (notification) => {
console.log('Foreground notification:', notification);
// Show in-app notification UI
});
// User tapped a notification
PushNotifications.addListener('pushNotificationActionPerformed', (action) => {
console.log('Notification tapped:', action.notification);
// Navigate to relevant screen
const data = action.notification.data;
if (data.screen === 'order') {
router.push(`/orders/${data.orderId}`);
}
});
}iOS — enable Push Notifications capability in Xcode and upload your APNs key to Firebase or your push service.
Android — add google-services.json to android/app/ from the Firebase console.
Fix 7: Platform Differences — iOS Xcode, Android Gradle, M-series Macs, Live Updates, PWA
The native toolchains are where most Capacitor builds break, and the symptoms rarely point at the real cause.
iOS Xcode build chain. Capacitor projects depend on a working CocoaPods install. On Apple Silicon Macs, the system-shipped Ruby produces native gems that target arm64, but some Pods (especially older ones using <sys/_types.h> patterns) need arch -x86_64 pod install until they ship arm64 binaries. The reliable fix is to install CocoaPods via Homebrew (brew install cocoapods) instead of system Ruby and to commit ios/App/Podfile.lock so the same Pod versions resolve on every machine. Xcode 15 also requires deployment target iOS 14 or higher for Capacitor 5, iOS 15 or higher for Capacitor 6, and Capacitor 7 raises that to iOS 16. Mismatching the Podfile platform :ios line with the Xcode project’s iOS Deployment Target is the most common “file list missing” cause.
Android Gradle chain. Android Studio bundles its own JDK (currently JDK 21), but ./gradlew from the terminal uses JAVA_HOME. If they disagree, the IDE builds and the CLI fails, or vice versa. Set JAVA_HOME to the JDK that Android Studio uses (File → Project Structure → SDK Location → Gradle JDK). Gradle 8.4+ is required for Capacitor 6, and Android Gradle Plugin 8.x rejects projects targeting compileSdkVersion below 34. When cap sync android rewrites variables.gradle, any manual edits to versions get clobbered — keep custom version pins in android/build.gradle outside the managed block.
Native plugin rebuilds on M-series Macs. Plugins with native modules (react-native-* ported wrappers, @capacitor-community/sqlite) sometimes ship pre-built binaries that target x86_64 only. The fix is pod install --repo-update after deleting ios/App/Pods and Podfile.lock, then a fresh npx cap sync ios. If a plugin’s Pod doesn’t have an arm64 slice, you have to build under Rosetta or upgrade the plugin. cap doctor will flag the broken pod once it’s installed.
Live Updates vs OTA web bundle updates. Capacitor has two separate “OTA” stories. The official Live Updates product (Ionic Appflow) downloads a new web bundle while the app is running and applies it on the next launch — no app store review needed for JS-only changes. The community pattern of @capgo/capacitor-updater does the same on a different backend. Either way, only changes inside webDir ship over the air; any change to native code, plugins, or capacitor.config.ts requires a real binary release. Teams ship a JS bug fix as a Live Update, then are surprised when a new native plugin doesn’t activate until the App Store build lands.
Native plugin registration. On iOS, Capacitor plugins must extend CAPPlugin and be exposed with @objc(MyPluginName). On Android, plugins must be annotated @CapacitorPlugin(name = "MyPluginName") and registered in MainActivity.java’s registerPlugins() call (Capacitor 4 and earlier) or auto-discovered via the @CapacitorPlugin annotation (Capacitor 5+). If Capacitor.isPluginAvailable('MyPlugin') returns false on a real device even though the npm package is installed, registration is the culprit.
PWA fallback. Many Capacitor plugins ship a web implementation that uses standard Web APIs — @capacitor/share falls back to navigator.share, @capacitor/preferences uses localStorage. The same Capacitor codebase can be deployed as a PWA by serving webDir as a static site and ignoring native plugins where Capacitor.getPlatform() === 'web'. The trap is plugins without a web implementation: they throw rather than no-op, so any unguarded call kills the page. Always wrap with if (Capacitor.isPluginAvailable('Plugin')) for shared code paths.
Still Not Working?
“Plugin not implemented on web” — this is expected. Most hardware plugins (Camera, Geolocation, Haptics) have no web implementation. Use Capacitor.isNativePlatform() to conditionally call native APIs and provide web fallbacks. Some plugins like @capacitor/preferences (formerly Storage) work on all platforms.
cap sync runs but the plugin doesn’t appear in the native project — the plugin’s native code must be registered. For iOS, run cd ios/App && pod install. For Android, the plugin should auto-register via Gradle. If it doesn’t, check that the plugin package is in package.json dependencies (not devDependencies).
App works in browser but white screen on device — the web build output isn’t being served. Check that webDir in capacitor.config.ts matches your build output directory. Run npm run build before npx cap sync. Also check the browser console in Safari (iOS) or Chrome DevTools (Android) for JavaScript errors.
Deep links don’t open the app — deep linking requires native configuration beyond Capacitor. For iOS, add Associated Domains in Xcode and host an apple-app-site-association file. For Android, add intent filters in AndroidManifest.xml and host an assetlinks.json file. Use @capacitor/app plugin to handle the incoming URL.
Build succeeds but the app crashes on launch on a specific iOS version — Capacitor’s minimum iOS version stepped from 13 to 14 to 15 to 16 across versions 4 → 5 → 6 → 7. If your Podfile platform :ios, 'X' is lower than what your Capacitor major requires, the build links against APIs that aren’t present on the device. Match the Podfile, the Xcode deployment target, and the Capacitor major version.
./gradlew assembleDebug fails with “Unsupported class file major version” — JDK mismatch. Android Gradle Plugin 8 needs JDK 17; AGP 8.5+ supports JDK 21. Check ./gradlew --version and set JAVA_HOME to the matching JDK. Android Studio’s bundled JDK is usually the right one.
Live Reload works on simulator but not on a physical device — the device is on a different network or the dev server is bound to 127.0.0.1 only. Run the dev server with --host 0.0.0.0, put both machines on the same Wi-Fi, and confirm the firewall on macOS / Windows allows inbound on the dev port. Corporate Wi-Fi often blocks peer traffic between clients — use a personal hotspot to test.
For related mobile issues, see Fix: Expo Not Working, Fix: React Native Reanimated Not Working, Fix: Gradle Build Failed, and Fix: React Native Metro Bundler Failed.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Deno PermissionDenied — Missing --allow-read, --allow-net, and Other Flags
How to fix Deno PermissionDenied (NotCapable in Deno 2) errors — the right permission flags, path-scoped permissions, deno.json permission sets, and the Deno.permissions API.
Fix: Fastify Not Working — 404, Plugin Encapsulation, and Schema Validation Errors
How to fix Fastify issues — route 404 from plugin encapsulation, reply already sent, FST_ERR_VALIDATION, request.body undefined, @fastify/cors, hooks not running, and TypeScript type inference.
Fix: Expo Router Not Working — Routes Not Matching, Layout Nesting Wrong, or Deep Links Failing
How to fix Expo Router issues — file-based routing, layout routes, dynamic segments, tabs and stack navigation, modal routes, authentication flows, and deep linking configuration.
Fix: React Native Paper Not Working — Theme Not Applying, Icons Missing, or Components Unstyled
How to fix React Native Paper issues — PaperProvider setup, Material Design 3 theming, custom color schemes, icon configuration, dark mode, and Expo integration.