Kraken nightly scan flagged Lifecycle.kt’s inner scope.launch { piManager.initPins() } as unstructured, hypothesising a JVM crash risk. On inspection the JVM-crash claim was wrong (IO_SCOPE already carries SupervisorJob + CoroutineExceptionHandler), but two real issues were confirmed:
piManager.initPins() produced only the generic “Uncaught exception in IO_SCOPE” log with no indication that GPIO was impacted — silent from an operator’s perspective.job.invokeOnCompletion always fired the startup-completion logic (node invocation, platformLogger.postLogs()) even when the outer startup job had failed, sending misleading telemetry and invoking the server node in a potentially broken state.The inner scope.launch block that runs initPins() had no try-catch, so any exception propagated to the scope’s generic handler. The invokeOnCompletion lambda did not inspect the cause parameter, so it ran unconditionally.
launchPinInit(scope, piManager, logger, onSuccess) from the inline block inside piManager.init { … }. The helper catches non-CancellationException throwables and logs a specific “Pi pin initialization failed — GPIO will be unavailable until restart” message; onSuccess is only invoked on the happy path.job.invokeOnCompletion { } to job.invokeOnCompletion { cause -> } with an early return + error log when cause != null, so node invocation and postLogs() only fire when startup succeeded.LifecyclePinInitTest with two cases (failure contained, success invokes onSuccess).() -> Unit lambda passed to an async init), always wrap the launched block with explicit try-catch rather than relying on the scope’s generic CoroutineExceptionHandler. The generic handler’s message (“Uncaught exception in IO_SCOPE”) gives no context about which sub-system failed.invokeOnCompletion lambdas should always inspect the cause: Throwable? parameter before doing meaningful work. A null cause means success; a non-null cause means the job was cancelled or threw — downstream work that assumes success should be guarded accordingly.launchProcessing in ProcessingScope.kt is the correct helper for processor coroutines; bare scope.launch is acceptable only in lifecycle/daemon paths, and those should carry their own try-catch with domain-specific messages.