
Debug Mobile Dependency & Package Manager Crashes
Few crash classes are as frustrating as the ones that happen before your own code even gets a chance to run. A dependency or package manager crash is a failure in the build graph, the linker, or the runtime loader: a dyld error at app launch, a Program type already present thrown by D8's dex merge, or a NoClassDefFoundError when a library your APK expects is missing at runtime. These are not logic bugs you wrote; they are configuration bugs baked into how your app assembles third-party code. This guide walks through the most common mobile dependency and package manager crash signatures across iOS and Android and shows you how to reproduce, fix, and prevent each one.
The Two Failure Surfaces
Every dependency crash happens at one of two boundaries. The first is build time: a resolver (Gradle, CocoaPods, Swift Package Manager, or npm) picks a set of versions, and a compiler or linker stitches them together. When two libraries pull in incompatible versions of the same transitive dependency, or when a symbol is declared twice, the build either fails outright or produces a binary that is already broken. The second is run time: the linker (dyld on Apple platforms) or the class loader (ART/Dalvik on Android) cannot find a framework, symbol, or class that the binary was compiled against. A build that "succeeds" on your machine can still crash in production if the assembled artifact is internally inconsistent. Understanding which surface you are on is the fastest way to know which tool to reach for.
iOS Linker & dyld Launch Crashes
On iOS the classic signature is the one Apple documents around embedding frameworks:
dyld[4821]: Library not loaded: @rpath/MyFramework.framework/MyFramework
Referenced from: /private/var/containers/Bundle/Application/.../MyApp.app/MyApp
Reason: image not loadedThis means the binary links against a framework that is not embedded in the app bundle, so the dynamic linker cannot resolve it at launch. The most common cause is a framework that is linked but not copied into the "Frameworks" build phase. Apple's guidance on embedding frameworks in your app explains the difference between a linked-but-not-embedded framework and one that is actually shipped. The fix is to add the framework to the Embed Frameworks phase (or, for a Swift Package, to ensure the target links the product) and to verify the runpath with otool -L. A related family is undefined symbols: the app compiled against a symbol that the linked library no longer exports, usually after a library upgraded and removed an API. You see this as Undefined symbol: _SomeSymbol at link time, which is straightforward, but the harder variant is a weak symbol that resolves at build time and traps at launch on a device with an older OS. If your crash happens in pre-main code, it is worth cross-referencing our cold start crash debugging guide, because many dyld failures surface identically to pre-main crashes.
Duplicate Symbols & Program Type Already Present
On Android, the analogous build-time explosion is D8's dex merge. When two dependencies both ship the same class, you get:
Type com.example.SomeClass is defined multiple times:
.../libA.jar:com/example/SomeClass.class
.../libB.jar:com/example/SomeClass.classThe classic trigger is a library that bundles a third-party dependency directly instead of declaring it as a transitive dependency, so your app ends up with two copies. R8/D8 shrink the class set before dexing, and duplicate classes are a hard error rather than a warning. This is closely related to the shrinking failures we covered in our ProGuard, R8 & DexGuard debugging guide, but the root cause here is resolution, not obfuscation. The fix is almost always to exclude one of the duplicates or align the versions. You can inspect the resolution graph with ./gradlew app:dependencies to find which artifact is pulling in the second copy, then exclude it:
implementation("com.example:libA:2.1.0") {
exclude(group = "com.example", module = "common")
}Runtime vs Compile Version Skew
The nastiest dependency crashes are the ones where the build succeeds but the runtime disagrees. On Android this shows up as NoClassDefFoundError, NoSuchMethodError, or ClassNotFoundException thrown deep inside a library. The pattern is always the same: your app compiled against version 2.0 of a library, but at runtime the class loader finds version 1.x — or no class at all — because a transitive dependency was bumped without a matching compile-time update. A NoSuchMethodError is the fingerprint of a method that existed at compile time but was removed or renamed in the version actually loaded. These crashes are frequently blamed on the wrong library, because the stack trace points at your code while the real problem is a version mismatch one hop away. Android's dependency configuration documentation covers how implementation and api affect which versions are exposed to consumers, and understanding that scoping is the first step to avoiding skew.
AndroidX vs Support Library Conflicts
A special case of duplicate classes is the AndroidX migration hazard: the old android.support.* and the new androidx.* packages contain the same classes under different names, and if any transitive dependency still pulls in the legacy support library, you get duplicated resources and — in the worst case — NoClassDefFoundError for androidx classes at runtime. Android's build tooling warns about this via the "jetified" messages, but a library that has not been jetified will silently reintroduce support-library classes. The fix is to enable android.enableJetifier=true or, better, to upgrade every dependency to a version that is AndroidX-native so you can disable jetifier entirely. Keeping a single consistent AndroidX version set — ideally through a version catalog — eliminates this class of bug for good.
CocoaPods: Lock Drift & Framework Search Paths
CocoaPods resolves versions through the Podfile but pins the result in Podfile.lock. When the lock file drifts from the Podfile — after someone edits a version constraint without running pod install — CI or a teammate's machine builds against a different set of pods than production. The CocoaPods guide to the Podfile syntax is the authoritative reference here. A second, more insidious failure is duplicate frameworks: when two pods embed the same third-party framework, or when a pod is both statically and dynamically linked, you hit duplicate-symbol or image not found errors at launch. Finally, framework search paths are a recurring cause of "header not found" at build time and "image not found" at runtime; when a pod's FRAMEWORK_SEARCH_PATHS is clobbered by a later build setting, the linker silently links against the wrong copy.
platform :ios, '15.0'
use_frameworks!
target 'MyApp' do
pod 'Alamofire', '~> 5.8'
pod 'Firebase/Analytics'
endThe discipline that prevents most of this is committing Podfile.lock and running pod install (never pod update casually) so every machine and CI builds the identical graph.
Swift Package Manager: Resolution & Binary Targets
Swift Package Manager resolves versions declaratively in Package.swift, and its resolver can produce surprising failures when two packages require incompatible versions of a shared dependency. The SPM package manager documentation describes how version ranges are unified. Two common crashes follow: a binary target ABI mismatch, where a prebuilt xcframework was compiled with a different Swift compiler version than the consuming app, producing a runtime dyld failure or a linker ABI error; and a language-version error, where the package declares a newer swift-tools-version than the toolchain in CI supports. The fix for the former is to pin the binary dependency to the exact version built with your toolchain, and for the latter to bump CI to the toolchain the package requires.
// swift-tools-version:5.9
import PackageDescription
let package = Package(
name: "MySDK",
platforms: [.iOS(.v15)],
products: [.library(name: "MySDK", targets: ["MySDK"])],
dependencies: [
.package(url: "https://github.com/Alamofire/Alamofire.git", from: "5.8.0")
],
targets: [
.target(name: "MySDK", dependencies: ["Alamofire"])
]
)Gradle: Resolution Strategies & Version Catalogs
Gradle's dependency resolution is where most Android dependency crashes originate, and Gradle gives you the sharpest tools to control it. The Gradle dependency resolution guide documents conflict resolution, and the version catalog documentation explains the modern way to centralize versions. A version catalog (libs.versions.toml) is the single most effective prevention tool you can adopt, because it forces every module to declare versions in one place:
[versions]
kotlin = "2.0.20"
coroutines = "1.8.1"
[libraries]
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }When a conflict still slips through, you can force a strict version or fail the build on any conflict so the ambiguity cannot silently ship:
configurations.all {
resolutionStrategy {
failOnVersionConflict()
force("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1")
}
}To diagnose which path is pulling in a bad version, dependencyInsight is the tool you want:
./gradlew app:dependencyInsight --dependency kotlinx-coroutines-core --configuration releaseRuntimeClasspathReact Native: Hoisting & Native Module Registration
React Native adds a JavaScript package layer on top of native builds, and its own crash signatures come from npm's hoisting behavior. When two packages depend on different versions of a native module, npm hoists one copy to the root node_modules, and the other copy's native code can end up registering the same native module twice — a duplicate-registration crash at startup — or loading a JS bundle built against a different native ABI than the one linked. The React Native libraries guide covers how native modules are discovered and linked. A second failure is JS/native version skew: the JavaScript side of a library updates in package.json while the native pods or Gradle artifacts stay pinned, so the JS calls a native method that no longer exists. The fix is a lockfile discipline — yarn.lock or package-lock.json committed, plus pod install after any dependency change — and a CI step that runs npx react-native config to verify native modules resolve cleanly.
Binary ABI Incompatibility
Many of the crashes above reduce to a single root cause: two binaries were compiled with incompatible ABIs. A Swift xcframework built with Swift 5.9 will not reliably link into an app built with Swift 5.8; a Kotlin Multiplatform library compiled against a different Kotlin compiler version than the consuming app can throw NoSuchMethodError at runtime; and a native .so built with one NDK version can fail to load against a device's linker. These are not package manager bugs in the strict sense, but the package manager is the place where the mismatch is introduced, which is why pinning compiler and toolchain versions in CI — alongside your dependency versions — is the only reliable defense.
Detection & Prevention
The cheapest dependency crash is the one you never ship. Commit every lockfile (Podfile.lock, Package.resolved, gradle.lockfile, yarn.lock); adopt version catalogs so versions live in one file; and add a CI dependency-diff gate that fails the build when the resolved graph changes between a pull request and its base branch, so a silent transitive bump is reviewed instead of discovered in production. On Android, run ./gradlew app:dependencies on every release to eyeball the full tree, and lean on dependencyInsight the moment a duplicate or a skew appears. When a dependency crash does reach users, the stack trace alone rarely tells the whole story — a NoClassDefFoundError points at your code while the real culprit is two hops away. Instrumenting your app so each release records its resolved dependency versions alongside crash reports lets you correlate a spike in crashes with a specific dependency bump instantly. Bugspulse attaches build and dependency metadata to every session, so you can see exactly which versions were live when a crash first appeared, instead of guessing from a stack trace.
Dependency and package manager crashes are the rare bug class where prevention is genuinely possible: a locked, version-cataloged, CI-gated dependency graph removes the entire category of dyld failures, duplicate symbols, and NoClassDefFoundError crashes before they ever reach a user. If you want that kind of visibility the moment something slips through, sign up and start tracking your releases with Bugspulse.