I think Kotlin coroutines are worth the learning curve because they make asynchronous code feel like normal code again.
That sounds like a small thing until you spend enough time in callback-heavy code, promise chains, or async code that has been split into too many little pieces. Then the difference becomes obvious. Coroutines do not make concurrency easy, but they do make it easier to read what the code is actually doing.
The main win is readability
The reason I keep liking coroutines is not that they are fashionable. It is that they make a function tell one story instead of three.
With callbacks, the control flow often gets scattered. You have to jump around to see where the work starts, where it finishes, and where errors go. With coroutines, the code usually reads in the order the work happens. That matters because most bugs in async code are really understanding problems, not typing problems.
When I can read a function top to bottom and understand the sequence of work, I trust it more.
They reduce callback-shaped mistakes
Callback code tends to create the same problems over and over.
You lose the shape of the work. Error handling gets duplicated or skipped. Nested logic starts to look like a staircase. A timeout or cancellation rule gets buried somewhere awkward. Eventually the code still works, but nobody wants to touch it.
Coroutines help because they let you write async logic without forcing everything into a callback shape. That does not mean the code is automatically good. It just means the code has a chance to stay honest about what it is doing.
Discipline still matters
Coroutines are not a free pass.
If you throw every async step into a different scope, bury logic behind layers of abstraction, or use flow operators just because they look elegant, you can still make the code hard to understand. The language gives you better tools. It does not remove the need for judgment.
The rule I try to keep in mind is simple: if the coroutine code still reads clearly, it is doing its job. If I have to mentally reconstruct the execution path every time, the code has gone too far.
That includes the boring parts too. Cancellation should make sense. Timeouts should be obvious. Errors should not disappear into a corner where nobody sees them. Async code gets ugly fast when those decisions are left vague.
The learning curve is real, but the payoff is too
I do not think coroutines are worth learning because they are clever. I think they are worth learning because they usually make the code easier to maintain once the project grows past the happy path.
That is the real test for me. A language feature is useful if it makes the next change less annoying, not just the first demo more impressive.
Coroutines pass that test more often than most async models I have used. They give you a way to keep sequencing readable without pretending concurrency does not exist. That is enough for me.