@@ -156,6 +156,93 @@ for t in threads:
156156 t.join()
157157```
158158
159+ ## Asyncio Support
160+
161+ For asyncio applications (FastAPI, Starlette, aiohttp, etc.), use ` erlang.async_call() `
162+ instead of ` erlang.call() ` . This integrates properly with asyncio's event loop without
163+ blocking or raising exceptions for control flow.
164+
165+ ### Basic Usage
166+
167+ ``` python
168+ import asyncio
169+ import erlang
170+
171+ async def fetch_data ():
172+ result = await erlang.async_call(' get_user' , user_id)
173+ return result
174+
175+ # Run in asyncio context
176+ asyncio.run(fetch_data())
177+ ```
178+
179+ ### With FastAPI/Starlette
180+
181+ ``` python
182+ from fastapi import FastAPI
183+ import erlang
184+
185+ app = FastAPI()
186+
187+ @app.get (" /user/{user_id} " )
188+ async def get_user (user_id : int ):
189+ # Use async_call for asyncio compatibility
190+ user = await erlang.async_call(' get_user' , user_id)
191+ return {" user" : user}
192+
193+ @app.websocket (" /ws" )
194+ async def websocket_endpoint (websocket ):
195+ await websocket.accept()
196+ while True :
197+ data = await websocket.receive_text()
198+ # Safe to use in WebSocket handlers
199+ result = await erlang.async_call(' process_message' , data)
200+ await websocket.send_text(result)
201+ ```
202+
203+ ### Concurrent Async Calls
204+
205+ ``` python
206+ import asyncio
207+ import erlang
208+
209+ async def parallel_queries ():
210+ # Run multiple Erlang calls concurrently
211+ results = await asyncio.gather(
212+ erlang.async_call(' query_a' , param1),
213+ erlang.async_call(' query_b' , param2),
214+ erlang.async_call(' query_c' , param3)
215+ )
216+ return results
217+ ```
218+
219+ ### Why Use async_call?
220+
221+ The standard ` erlang.call() ` uses a suspension mechanism that raises a
222+ ` SuspensionRequired ` exception internally. While this works in most contexts,
223+ it can cause issues with ASGI middleware that catches and handles exceptions:
224+
225+ - ASGI middleware (Starlette, FastAPI) catches exceptions during request handling
226+ - The ` SuspensionRequired ` exception propagates through middleware layers
227+ - asyncio logs "Task exception was never retrieved" warnings
228+
229+ ` erlang.async_call() ` avoids these issues by:
230+ - Not raising exceptions for control flow
231+ - Integrating with asyncio's event loop via ` add_reader() `
232+ - Releasing the dirty NIF thread while waiting (non-blocking)
233+ - Using a Future that resolves when the Erlang callback completes
234+
235+ ### When to Use Each
236+
237+ | Context | Recommended API |
238+ | ---------| -----------------|
239+ | Synchronous Python code | ` erlang.call() ` |
240+ | Threading (` threading.Thread ` ) | ` erlang.call() ` |
241+ | ThreadPoolExecutor | ` erlang.call() ` |
242+ | Asyncio (async/await) | ` erlang.async_call() ` |
243+ | FastAPI/Starlette | ` erlang.async_call() ` |
244+ | ASGI applications | ` erlang.async_call() ` |
245+
159246## Error Handling
160247
161248Errors from Erlang functions are raised as Python exceptions:
@@ -174,3 +261,18 @@ thread = threading.Thread(target=worker)
174261thread.start()
175262thread.join()
176263```
264+
265+ ### Async Error Handling
266+
267+ ``` python
268+ import asyncio
269+ import erlang
270+
271+ async def safe_call ():
272+ try :
273+ result = await erlang.async_call(' maybe_fail' , 42 )
274+ return result
275+ except RuntimeError as e:
276+ print (f " Erlang error: { e} " )
277+ return None
278+ ```
0 commit comments