Skip to content
Logo

Wrapping async code

coroutine defers an awaitable the way sync defers a thunk: the thunk builds a fresh awaitable on every run, because a coroutine object can be awaited only once. Exceptions become defects. attempt_async is the attempt counterpart that maps expected exceptions into the error channel. run_main and the run_async family can interpret either:

 = httpx.()
 
E.(lambda: .())  # Effect[httpx.Response] — nothing awaited yet
 
 
def (: ) -> E.[, ]:
    async def () -> :
         = await .()
        .()
        return .
 
    def (: ) -> :
        if (, httpx.):
            return (url=, status_code=..)
        raise   # anything else stays a defect
 
    return E.(, )
 
 
E.(("https://example.com"))  # text, or raises HttpStatusError
 
E.(
    ("https://example.com")
)  # Succeeded(text) or Failure(Fail(HttpStatusError(...)))

Inside @E.gen bodies, yield from E.coroutine(...) works like any other effect; the generator itself stays synchronous. If the task running run_async_coroutine is cancelled, the effect unwinds with an Interrupt cause, which catch_all and catch skip like a Die: finalizers and scope releases run, and the run settles as Failure(Interrupt(exception)), so an asyncio.timeout around run_async_coroutine doesn't leak resources and completes normally with that Exit. The cancellation is consumed by run_async_coroutine; a caller whose task should stop re-raises the carried exception. A finalizer that is mid-await when the cancellation arrives is shielded and runs to completion, and a cancellation raised from a synchronous thunk or callback, such as a cancelled future's result(), unwinds the same way.

More examples: test_run_async.py.