Brief
The what: A quick snapshot of the context
I was in a team meeting, while setting up a new Coroutine primitives module.
When our CTO said something that stopped me mid-keystroke.
"We lost around $30 000 on the latest issue, due to a lack of observability in the project."
I looked at the code I had open and realized:
"This is the place we'll lose the next $100 000."

The code I was looking at
Every tutorial recommends this CoroutineScope setup and most projects ship it exactly like this.
This isn't a bug.
It's the gap between knowing Coroutines and understanding them.
I call it: "Silent Exceptions" ...

🐞 The Issue: Nothing crashes. Features just die quietly.

Breakdown
The when: Reverse engineering the cause and the cost
Any operation you launch from a global scope - syncing data, running optimistic requests, parallel background work - can throw an exception that disappears.
Let me show you exactly how this type of issue reaches production.
And what it costs when it does.
Lost data = lost money $$$
I'll take a sync data operation as an example.
Let's say you need to display a promo banner on the Feed.
Usually, such data loading consists of two steps:
Getting personalised configurations from the remote config:
{
"offer_id": "intro_50_off",
"banner_type": "LARGE"
}Loading offer details from the BE:
class UpdateUserPromosUseCase(
val configRepository: ConfigRepository,
val promoRepository: PromoRepository
){
suspend fun invoke() {
val config = configRepository.getPromoBanner()
promoRepository.updatePromoBanner(
id = config.offer_id,
type = config.banner_type
)
}
}Since you don't want to increase app startup time - you'll launch this operation from the Application class.
In order to do that, you need to set up a global CoroutineScope:
@Module
class CoroutineScopeModule {
@Singleton
@Provides
@Named("ApplicationScope")
internal fun provideApplicationScope(
@Named("IODispatcher")
dispatcher: CoroutineDispatcher,
): CoroutineScope = CoroutineScope(dispatcher + SupervisorJob())
}Then use it to start an asynchronous update operation:
class MyApplication : Application() {
//.... dependencies ...
override fun onCreate() {
applicationScope.launch {
updateUserPromoUseCase()
}
}
}Nothing suspicious. Right?
You build it. You test it. You ship it.
Your users use it for a couple of weeks.
Then on Friday evening, your PO decides to run a small A/B test.
He updates the remote config for a specific user segment:
{
"offer_id": "intro_50_off",
"banner_type": "SMAL"
}The setup was fine, except for a little typo: SMALL → SMAL .
(The app silently threw a serialisation exception trying to parse an unknown enum value)
On Monday morning he checks the analytics - nothing changed.
He asks devs: "Any crashes?" - "No crashes. The app is stable."
He asks QA: "Any issues on prod?" - "Banner displayed fine on dev and prod."
Because the test had a niche setup tied to user location, no one could reproduce it.
You spend a full day until you figure out the config issue.
Time and budget burned.
SupervisorJob protects your app. Not your features.
Here's what happens every time an exception fires in this setup:
An
Exceptionoccurs and the request failsException propagates up to the
SupervisorJobSupervisorJobsees it, isolates it, doesn't cancel siblingsLooks for a
CoroutineExceptionHandlerin the contextFinds none
Passes it to the platform's last-resort handler
The exception is silently swallowed
→ Feature never displayed.
This context has two elements when it ALWAYS needs three:
Dispatchertells coroutines where to runJobtells them how to failExceptionHandlermakes unhandled exceptions handled
SupervisorJob does exactly one thing — it prevents a failing coroutine from cancelling its siblings. That's it.
It doesn't catch the exception. It doesn't log it. It doesn't surface it anywhere.
It just isolates it. And lets it disappear.

✅ The Fix: Add CoroutineExceptionHandler into the Context and dispatch exceptions to observability tools.

Guideline
The how: Implementation guideline and recommendations
Make exceptions visible
The fix is simple and consists of just 3 steps.
Step 1 - Set up
Provide exception handler as a standalone dependency.
This way you can reuse its logging for other scopes as well.
@Retention
@Qualifier
annotation class ExceptionHandler {
@Retention
@Qualifier
annotation class Log
}
@Module
class CoroutineExceptionHandlerModule {
@Provides
@Singleton
@ExceptionHandler.Log
fun providesLogExceptionHandler(): CoroutineExceptionHandler =
CoroutineExceptionHandler { _, exception ->
//observability entry point here
}
}⚠ Use annotation class instead of @Named qualifier for type-safety.
Step 2 - Catch
Add the exception handler to the coroutine scope context:
@Retention
@Qualifier
annotation class Scope {
@Retention
@Qualifier
annotation class IO
}
@Module
class CoroutineScopesModule {
@Provides
@Singleton
@Scope.IO
fun providesIoCoroutineScope(
@Dispatcher.Io dispatcher: CoroutineDispatcher,
@ExceptionHandler.Log exceptionHandler: CoroutineExceptionHandler
): CoroutineScope = CoroutineScope(SupervisorJob() + dispatcher + exceptionHandler)
}Step 3 — Observe
Dispatch the collected exception to the observability tool you're using.
It could be a simple Log, Firebase Crashlytics, Grafana, DataDog, etc.
CoroutineExceptionHandler { _, exception ->
//Log
log(Level.SEVERE, "Unhandled coroutine exception", exception)
//Or report
Crashlytics.recordException(exception)
}One handler at scope level. Full coverage.
Nothing slips through silently again.
Let the AI agent do it
I already spent the time so you don't have to.
This isn't a "fix it" prompt - it's a full audit turned into a precise agent workflow.
You get a scanned codebase, confirmed changes, and clean implementation:
Use this issue as the implementation guideline:
👉 [ISSUE_LINK]
⛔ Do not proceed to the next step without explicit user confirmation.
⛔ Make sure user inserted ISSUE_LINK
⛔ If you can't process ISSUE_LINK ask user to copy-paste newsletter text
🔎 Step 1 — Observability
Scan for observability SDKs (Crashlytics, DataDog, Grafana, etc).
Present findings as a list and ask the user which one to use.
If none found, suggest adding one or default to Logcat.
🐞 Step 2 — Identify
Scan for all CoroutineScopes missing a CoroutineExceptionHandler.
Present results as a table: file path | scope name | missing element.
Ask the user to confirm before proceeding.
✅ Step 3 — Fix
Follow the Guideline section from the link above EXACTLY.
Implement each change directly in the codebase.
If any blocker is found, ask for clarification.
Pitch
The why: How to get it approved and get recognized
Before you do anything
Don't just forward this issue to your team.
That's the entry-level move.
The senior engineer move is showing up with numbers.
Spend 10 minutes in your codebase first:
Find the affected scopes - search for
CoroutineScopemissing aCoroutineExceptionHandlerPick 2–3 critical flows - payment, data sync, promos, onboarding - anything where a silent failure costs something real
Put a number on it - "If our payment job fails silently, that's roughly $X in unprocessed transactions"
You don't need exact numbers. A reasonable estimate is enough.
Leverage on AI research
10x engineers don't grep through the codebase manually anymore.
They delegate the research and show up with numbers.
This prompt scans your codebase for the issue above and returns a risk table ready to drop into the message below.
Use this issue as the context for the research:
👉 [INSSUE_LINK]
⛔ Make sure user has inserted ISSUE_LINK.
⛔ If you can't process ISSUE_LINK, ask user to copy-paste the newsletter text.
💰 Find the risk
Scan the codebase for the issue described in the ISSUE_LINK "Brief" and "Breakdown" sections.
For each affected scope identify the business operation it runs.
Present results as a table: reference | scope name | business operation | issue cost (~X [unit] per [day/week]).
The prompt output example
The message
This is your script. Send it to whoever needs to approve the fix - team lead, manager, or the whole team chat.
Hey [name],
I found a silent exceptions risk in our codebase I want to flag.
Our global workflows are missing observability. Any exception thrown from that jobs gets silently swallowed — no crash, no log, no alert. The app looks healthy while features quietly stop working.
I checked our most critical flows. [Flow 1] and [Flow 2] are both affected. Based on [your estimate], the risk is roughly [number] per [day/week] if left unresolved.
The fix is three lines of code and takes less than an hour. I'd like to prioritise it in the next sprint.
Happy to walk you through it if helpful.
→ You walk in as the engineer who found the problem, quantified it, and came with the solution ready.
That's not just a fix. That's a promotion conversation waiting to happen 😎

Explore
The where: Related resources and topics
I mentioned annotation class for DI qualifiers earlier in this issue. This is where I first wrote it all out - worth a look if you want the full picture.
It doesn't always fire when you expect it to. Worth reading through the pitfalls section before you ship anything.
Hidden exception handling strategy most Android devs never really explore. Worth knowing - but watch out for the coroutines pitfalls.

So here's my question to you this week:
Do you trust your crash rate? Reply yes or no — and one sentence on why. I'll publish the split next issue.