Troubleshooting
Most issues with Enro at runtime fall into one of a few categories â the controller isnât installed, a destination isnât bound to its key, a handle is accessed from the wrong scope, or a value canât be serialized. This page walks through the common symptoms.
If you donât find your issue here, please open an issue at github.com/isaac-udy/Enro/issues with a stack trace and a brief description of the call site.
Setup errors
EnroController has not been installed
The controller wasnât installed before something tried to use it. Make sure you call installNavigationController(...) on your NavigationComponent before any composable that reads a navigation handle is composed.
// Android
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
MyComponent.installNavigationController(this) // <- this must run first
}
}
See Installation for the equivalent on each platform.
No NavigationHandle found for [KeyClass]
You called navigationHandle<MyKey>() from a composable that isnât a navigation destination, or whose key is a different type.
A handle is only available inside a destination â that is, a composable function annotated with @NavigationDestination(...) (or the body of a navigationDestination<K> { } provider), or any composable composed inside one. The typed parameter must match the destinationâs actual key type.
@Composable
@NavigationDestination(ShowProfile::class)
fun ProfileScreen() {
val navigation = navigationHandle<ShowProfile>() // â
matches the annotation
// ...
}
@Composable
fun SomeRandomComposable() {
val navigation = navigationHandle<NavigationKey>() // â not inside a destination
}
If your composable is genuinely shared between destinations and doesnât need to know its key type, use the untyped form navigationHandle<NavigationKey>() and accept that navigation.key will be the base interface.
No LocalNavigationHandle
Same family as the previous error â composable reading the navigation handle from outside any destination. Wrap it inside a destination, or hoist the navigation calls up to a destination-level composable and pass the operations down as lambdas.
Missing navigation binding for [KeyClass]
Your key is being opened but no destination is registered for it. Three common causes:
- Forgot
@NavigationDestination(KeyClass::class)on the destination. The annotation is what drives KSP code generation. - The module that declares the destination doesnât apply the
enro-processorKSP plugin. Every module that contains@NavigationDestinationannotations needs its own KSP dependency ondev.enro:enro-processor. - The app module doesnât depend on the destination module. KSP-generated bindings only travel through direct dependencies â make sure your app moduleâs classpath includes every feature module that contains destinations.
If all three look correct, try a clean build (./gradlew clean :app:assembleDebug) â KSP incrementality can occasionally cache an old result. See the modular navigation recipe for the canonical multi-module setup.
Navigation-handle errors
${key} is a NavigationKey.WithResult and cannot be completed without a result
You called navigation.complete() (no arguments) on a destination whose key implements NavigationKey.WithResult<R>. A result-producing key must produce a result.
// â won't compile if the key implements WithResult<LocalDate>
navigation.complete()
// â
pass the result
navigation.complete(LocalDate.now())
If the destination genuinely has no result to deliver and you want it gone, use navigation.close() â the callerâs onClosed callback will fire.
Cannot completeFrom a NavigationKey.WithResult from a NavigationKey that does not also implement NavigationKey.WithResult
completeFrom(otherKey) says âdelegate this destinationâs completion to otherKeyâ. For that to make sense, both keys must agree on the result type: a WithResult<R> destination can only completeFrom another WithResult<R> with the matching R.
If the redirect target genuinely doesnât have a result, you probably want closeAndReplaceWith(otherKey) instead.
Multiple onCloseRequested callbacks registered for the same NavigationHandle
You registered the close-requested callback in more than one place for the same destination â typically one in the ViewModel and one in the Composable, or two configure { } blocks in the same composable. Only one callback may be active at a time.
Pick one home for the callback (the ViewModel if you have one, otherwise the top-level Composable). See Navigation Handles â Overriding the close-requested callback.
Cannot execute NavigationOperation on TestNavigationHandle that is closed
The handle youâre using in a test was closed (or completed) and then you tried to drive more navigation through it. Either the test is exercising a flow that goes past the close, or the assertion is at the wrong point.
If you intentionally want to continue using the handle after a close, call handle.clearOperationHistory() between scenarios.
SyntheticDestination for [Key] has already finished with [Outcome]
An outcome method (open, close, closeSilently, complete, completeFrom) was called on a syntheticâs scope after the synthetic had already concluded. The dispatcher catches the first outcome the block emits and converts it to a navigation operation; subsequent calls have nothing to do.
The usual cause is launching a coroutine inside a synthetic block:
@NavigationDestination(BadSynthetic::class)
val badSynthetic = syntheticDestination<BadSynthetic> {
context.lifecycleOwner.lifecycleScope.launch {
someAsyncWork()
complete() // â block has long since returned; throws "already finished"
}
}
Synthetics are intentionally synchronous decision points â they donât have their own lifecycle, view-model store, or persisted state. The fix is one of:
- Do the async work before opening the synthetic and pass the result into the syntheticâs key as a parameter.
- Forward to a real destination that owns the work as part of its own lifecycle:
syntheticDestination<...> { completeFrom(LoadingScreen) }. - Restructure as a regular destination with a loading state if the synthetic was really trying to be a âdo some work then closeâ UI.
See Synthetic Destinations for the full design rationale.
Result errors
Received result for id ${id}, but no active steps had that id
Specific to managed flows. The flow received a step result, but the flow scope no longer contains a step matching that id â usually because the flowâs body changed shape between the time the step was opened and the time the result came back (an upstream value caused a branch to no longer include this step, for example).
Make sure each open(key) inside the flow is reached deterministically from the upstream results. Donât write conditions that produce a different ordering of open calls on re-evaluation; conditions are fine, but the same upstream results should always lead to the same flow shape.
Serialization errors
Object of type X could not be added to NavigationKey.Metadata
Youâre storing a value in instance.metadata whose serializer isnât registered. Built-in Kotlin types are fine; custom types either need to be @Serializable or have a serializer registered on the NavigationComponent:
@NavigationComponent
object MyComponent : NavigationComponentConfiguration(
module = createNavigationModule {
serializersModule(SerializersModule {
contextual(MyCustomType.serializer())
})
}
)
Backstack restoration fails after process death
Usually means one of your NavigationKeys has a non-serializable property. Mark the key as @Serializable and make sure every property on it is also serializable (either built-in, @Serializable itself, or registered in your SerializersModule). Run a simple âkill from Recentsâ test in development to flush these out early.
Testing errors
No NavigationHandle found ... in a test
Youâre constructing a destination or ViewModel without setting up a test controller. Wrap the test in runEnroTest { }, or add an EnroTestRule to the test class.
@Test
fun example() = runEnroTest {
val handle = createTestNavigationHandle(MyKey)
// ...
}
For ViewModels with by navigationHandle<MyKey>(), also call putNavigationHandleForViewModel<MyVm, MyKey>(key) so the ViewModel can resolve its handle.
See Testing.
Multiple onCloseRequested callbacks ... in a test
Same root cause as in app code, but easier to hit accidentally if a fixture installs the callback and the testâs before block does too. Make sure each scenario only sets up the callback once.
Migration errors (Enro 2 â 3)
If youâre getting compile errors after upgrading from Enro 2, see the migration guide. The most common ones:
Unresolved reference: SupportsPush/SupportsPresentâ the v2 markers are gone. Use bareNavigationKeyorNavigationKey.WithResult<R>.Unresolved reference: closeWithResultâ renamed tocomplete(value).Unresolved reference: push/presentâ both replaced byopen. Dialog/overlay behaviour moves into destination metadata.Unresolved reference: NavigationApplicationâ gone. Install the controller directly fromApplication.onCreate.@Parcelizeflagged but not migrated â keys are now@Serializable(kotlinx.serialization).enroViewModelsno longer resolves â useviewModel { createEnroViewModel { ... } }.
Reporting an issue
If your problem isnât covered here, the most helpful issue report includes:
- The full stack trace.
- A minimal code snippet showing the call site.
- The Enro version youâre on (e.g.
3.0.0-beta01). - The platform (Android API level, iOS version, JDK version, browser).