Callback style, within an I/O handler, is (almost always) a concession to the event loop. When I'm writing a program, I write:
routine():
result = do_one_thing()
do_next_thing(result)
do_whatever()
With callback based I/O, you need the routine to end so the "reactor" up the call stack can get control again to do I/O on your behalf: routine_1():
return make_some_io_request(callback=routine_2)
routine_2():
return make_another_io_request(callback=routine_3)
routine_3():
return send_response_to_original_socket()
Now, if you were desperate, you could just call over to the reactor like this: routine():
resp = do_io_for_me()
next_resp = do_more_io_for_me(resp)
return transform(next_resp)
.. but, there are (at least) two problems with this:1. You're making the stack deeper every time you "call over" to the reactor... you will stack overflow eventually
2. In most languages, the reactor doesn't have a way to "call back into" your stack frame and resume it.
Coroutines are a way to "call over" to the reactor, have your stack state frozen (in heap space, without going deeper in the call stack), and then "unfreeze it" later on, when the reactor has completed the IO.
The greenlet package provides this for Python; Haskell and Erlang have native support for this. And, this is ultimately how you want to program. A(); B(); C(). Sure, there are exceptions for UI driven callbacks, and callback patterns aren't all bad, but typically in network IO cases, the callbacks are explicitly a concession to the I/O loop's need to be scheduled, made necessary by a missing language or vm feature.
And: cleaner code benefits 200 LOC apps as much as it does 200k LOC apps. A good design is a good design at any scale.