Ant implements a full-featured event loop that handles asynchronous operations, timers, I/O, and microtasks. The event loop is built on top of libuv and integrates seamlessly with JavaScript’s async/await syntax.
Event loop architecture
The event loop continuously polls for work and processes different types of tasks in priority order:
// From reactor.c - work types
typedef enum {
WORK_MICROTASKS = 1 << 0 , // Promise callbacks, queueMicrotask
WORK_TIMERS = 1 << 1 , // setTimeout, setInterval
WORK_IMMEDIATES = 1 << 2 , // setImmediate
WORK_COROUTINES = 1 << 3 , // Async functions
WORK_COROUTINES_READY = 1 << 4 , // Ready-to-run coroutines
WORK_FETCHES = 1 << 5 , // fetch() operations
WORK_FS_OPS = 1 << 6 , // File system operations
WORK_CHILD_PROCS = 1 << 7 , // Child processes
WORK_READLINE = 1 << 8 , // Readline interfaces
WORK_STDIN = 1 << 9 , // Standard input
} work_flags_t ;
Main event loop
The core event loop implementation processes tasks until no work remains:
// From reactor.c - main event loop
void js_run_event_loop ( ant_t * js ) {
drain:
while ( event_loop_alive ()) {
js_poll_events (js);
work_flags_t work = get_pending_work ();
if (work & WORK_BLOCKING)
uv_run ( uv_default_loop (), UV_RUN_NOWAIT);
else if ((work & WORK_ASYNC) || UV_CHECK_ALIVE) {
js_gc_maybe (js);
uv_run ( uv_default_loop (), UV_RUN_ONCE);
} else break ;
}
js_poll_events (js);
jsval_t code = js_mknum ( 0 );
emit_process_event ( "beforeExit" , & code, 1 );
if ( event_loop_alive ()) goto drain;
}
Event loop phases
Each iteration of the event loop processes work in this order:
Immediates - setImmediate() callbacks
Microtasks - Promise callbacks and queueMicrotask()
Coroutines - Ready async function continuations
I/O polling - libuv handles timers, network, file system
Before exit - process.on('beforeExit') event
Task scheduling
Microtasks
Microtasks run before the next event loop iteration:
queueMicrotask (() => {
console . log ( 'Microtask' );
});
Promise . resolve (). then (() => {
console . log ( 'Promise microtask' );
});
console . log ( 'Synchronous' );
// Output:
// Synchronous
// Microtask
// Promise microtask
Timers
Timers schedule callbacks to run after a delay:
// setTimeout: run once after delay
setTimeout (() => {
console . log ( 'Timeout' );
}, 1000 );
// setInterval: run repeatedly
const interval = setInterval (() => {
console . log ( 'Interval' );
}, 500 );
// clearInterval: stop repeating timer
setTimeout (() => clearInterval ( interval ), 2000 );
setImmediate() schedules callbacks to run in the next event loop iteration:
setImmediate (() => {
console . log ( 'Immediate' );
});
setTimeout (() => {
console . log ( 'Timeout' );
}, 0 );
console . log ( 'Synchronous' );
// Output:
// Synchronous
// Immediate
// Timeout
Asynchronous I/O
File system operations
File system operations are non-blocking and integrate with the event loop:
import { readFile , writeFile } from 'fs/promises' ;
// Read file asynchronously
const content = await readFile ( 'file.txt' , 'utf-8' );
console . log ( content );
// Write file asynchronously
await writeFile ( 'output.txt' , 'Hello, Ant!' );
Network operations
Network requests use the event loop for non-blocking I/O:
// Fetch is fully async
const response = await fetch ( 'https://api.example.com/data' );
const data = await response . json ();
console . log ( data );
// Multiple concurrent requests
const [ users , posts ] = await Promise . all ([
fetch ( '/api/users' ). then ( r => r . json ()),
fetch ( '/api/posts' ). then ( r => r . json ())
]);
HTTP servers
Servers handle requests asynchronously:
import { createServer } from 'http' ;
const server = createServer ( async ( req , res ) => {
// Async request handling
const data = await fetchDataFromDatabase ();
res . writeHead ( 200 , { 'Content-Type' : 'application/json' });
res . end ( JSON . stringify ( data ));
});
server . listen ( 3000 );
Ant’s event loop is highly optimized for performance. Here’s a benchmark example:
// From examples/demo/event_loop.js
const DURATION = 5_000 ;
const origin = performance . now ();
let count = 0 ;
function iter () {
count ++ ;
if ( performance . now () - origin < DURATION )
setImmediate ( iter );
}
iter ();
process . once ( 'beforeExit' , () => {
const elapsed = performance . now () - origin ;
const rate = count / ( elapsed / 1000 );
console . log ( ` ${ ( rate / 1_000_000 ). toFixed ( 2 ) } M iterations/sec` );
});
Typical performance: 1-2M+ event loop iterations per second
Coroutines and async functions
Ant uses coroutines (powered by minicoro) to implement async/await:
// From reactor.c - coroutine execution
for ( coroutine_t * c = pending_coroutines.head; c; c = c -> next) {
if ( c -> is_ready && c -> mco && mco_status ( c -> mco ) == MCO_SUSPENDED) {
temp = c;
break ;
}
}
if ( ! temp) break ;
temp -> is_ready = false ;
mco_result res;
MCO_RESUME_SAVE (js, temp -> mco , res);
if (res != MCO_SUCCESS || mco_status (temp -> mco ) == MCO_DEAD) {
remove_coroutine (temp);
free_coroutine (temp);
}
Async function lifecycle
Creation : Async function creates a coroutine
Suspension : await suspends the coroutine
Resumption : When awaited value resolves, coroutine resumes
Completion : Coroutine finishes and returns value
Process events
Ant supports process lifecycle events:
// beforeExit: event loop is empty but process hasn't exited
process . on ( 'beforeExit' , ( code ) => {
console . log ( 'Process beforeExit with code:' , code );
// Can schedule more async work here
});
// exit: process is about to exit (no more async work)
process . on ( 'exit' , ( code ) => {
console . log ( 'Process exit with code:' , code );
// Only synchronous code here
});
Custom poll hooks
You can register custom hooks to run during event loop polling:
// From reactor.h
typedef void ( * reactor_poll_hook_t )( void * data);
void js_reactor_set_poll_hook ( reactor_poll_hook_t hook , void * data );
Best practices
Avoid blocking the event loop
CPU-intensive operations block the event loop and prevent other tasks from running. Consider:
Breaking work into chunks with setImmediate()
Using worker threads for heavy computation
Offloading to native code with FFI
Handle promise rejections
Unhandled promise rejections can cause issues: // Always handle rejections
promise . catch ( err => console . error ( err ));
// Or use try/catch with async/await
try {
await riskyOperation ();
} catch ( err ) {
console . error ( err );
}
Always clean up timers, intervals, and event listeners: const timer = setTimeout (() => {}, 1000 );
// Later...
clearTimeout ( timer );
const interval = setInterval (() => {}, 100 );
// Later...
clearInterval ( interval );
Debugging the event loop
Enable event loop debugging:
# Enable VM debugging
export ANT_DEBUG = "dump/vm:all"
# Run with verbose logging
ant --verbose script.js
Use process.on('beforeExit') to detect when the event loop is empty. This is useful for performance measurements and ensuring all async work completes.
Next steps
Async/await Deep dive into async/await implementation
Runtime architecture Understand the runtime internals