5.17 Evaluate module
This is what we were waiting for! This is why we took so many steps. This is the time to run our code. In the v8 world, running a module is called module evaluation. Let's see how our hello world program gets evaluated.
Once all modules are fetched, cached, loaded, transpiled, compiled, and instantiated, it's time to execute the application. Just like a C/C++/Java code starts with the main program, a JS code starts to run from the main module or the root module.
Right after finishing the loading, the worker calls JS runtime's mod_evaluate function to evaluate the root module. The input of mod_evaluate function is the module id of the root module.
pub async fn execute_module(
&mut self,
module_specifier: &ModuleSpecifier,
) -> Result<(), AnyError> {
let id = self.preload_module(module_specifier).await?;
self.wait_for_inspector_session();
self.js_runtime.mod_evaluate(id).await
}
JS runtime's mod_evaluate function finishes when the module evaluation finishes. In simple cases, this would mean that the program is done. However, in complicated cases, Deno would go into the event loop for processing pending evaluation, ops, imports, etc.
The first module evaluation function is mod_evaluate and that's quite simple. There are two simple steps:
- Call mod_evaluate_inner
- This is the function that'll call v8's evaluate function
- Wait for evaluation to finish (poll_event_loop)
- Module evaluation finishes when
- An error has occurred, or
- There is nothing left to do
pub async fn mod_evaluate(&mut self, id: ModuleId) -> Result<(), AnyError> {
let mut receiver = self.mod_evaluate_inner(id);
poll_fn(|cx| {
if let Poll::Ready(maybe_result) = receiver.poll_next_unpin(cx) {
debug!("received module evaluate {:#?}", maybe_result);
let result = maybe_result.unwrap_or(Ok(()));
return Poll::Ready(result);
}
let _r = self.poll_event_loop(cx)?;
Poll::Pending
})
.await
}
The mod_evaluate_inner function is a bit bigger. Following illustration shows the steps this function takes:

Here is the abbreviated source of mod_evaluate_inner():
fn mod_evaluate_inner(
&mut self,
id: ModuleId,
) -> mpsc::Receiver<Result<(), AnyError>> {
self.shared_init();
let state_rc = Self::state(self.v8_isolate());
let context = self.global_context();
let scope = &mut v8::HandleScope::with_context(self.v8_isolate(), context);
let module = state_rc
.borrow()
.modules
.get_handle(id)
.map(|handle| v8::Local::new(scope, handle))
.expect("ModuleInfo not found");
let mut status = module.get_status();
let (sender, receiver) = mpsc::channel(1);
if status == v8::ModuleStatus::Instantiated {
let maybe_value = module.evaluate(scope);
// Update status after evaluating.
status = module.get_status();
if let Some(value) = maybe_value {
assert!(
status == v8::ModuleStatus::Evaluated
|| status == v8::ModuleStatus::Errored
);
let promise = v8::Local::<v8::Promise>::try_from(value)
.expect("Expected to get promise as module evaluation result");
let promise_global = v8::Global::new(scope, promise);
let mut state = state_rc.borrow_mut();
state.pending_promise_exceptions.remove(&promise_global);
let promise_global = v8::Global::new(scope, promise);
assert!(
state.pending_mod_evaluate.is_none(),
"There is already pending top level module evaluation"
);
state.pending_mod_evaluate = Some(ModEvaluate {
promise: promise_global,
sender,
});
scope.perform_microtask_checkpoint();
} else {
assert!(status == v8::ModuleStatus::Errored);
}
}
receiver
}
Here are the steps in detail:
- 1.Get module handle from module id
- 2.Call v8's module evaluate function
- 3.Check for immediate evaluation errors
- 4.Get a global promise for this module's scope
- 5.Save global promise into pending_mod_evaluate
- 6.Return the receiver to get async events later
After submitting the module for evaluation, JS runtime enters the asynchronous event loop. The event loop is quite big. It checks for a lot of things including the result of the top-level promise i.e. global promise which was created when the main module was submitted for evaluation.
The event loop has many parts and it's a bit premature to discuss them now. In the context of our hello world program, the only relevant piece of the event loop is:
- Checking pending module evaluate (pending_mod_evaluate)
- Checking promise rejections

The other work of the event loop is a black box for now. The reason being that our example is very simple. There are no asynchronous calls, no complex dynamic imports, no ops, no listens, etc. The hello world program would start and finish by itself. There is no other dependency to keep it running. The global promise associated with the main module would contain the result of the evaluation of the hello world program. It is important to note that the result of the program isn't the output of the program. The output of the program goes to a separate place. The result of the evaluation comes back to the event loop through the global promise.
Here is the complete code of a tick of event loop:
pub fn poll_event_loop(
&mut self,
cx: &mut Context,
) -> Poll<Result<(), AnyError>> {
self.shared_init();
let state_rc = Self::state(self.v8_isolate());
{
let state = state_rc.borrow();
state.waker.register(cx.waker());
}
// Ops
{
let overflow_response = self.poll_pending_ops(cx);
self.async_op_response(overflow_response)?;
self.drain_macrotasks()?;
self.check_promise_exceptions()?;
}
// Dynamic module loading - ie. modules loaded using "import()"
{
let poll_imports = self.prepare_dyn_imports(cx)?;
assert!(poll_imports.is_ready());
let poll_imports = self.poll_dyn_imports(cx)?;
assert!(poll_imports.is_ready());
self.evaluate_dyn_imports();
self.check_promise_exceptions()?;
}
// Top level module
self.evaluate_pending_module();
let state = state_rc.borrow();
let has_pending_ops = !state.pending_ops.is_empty();
let has_pending_dyn_imports = !{
state.preparing_dyn_imports.is_empty()
&& state.pending_dyn_imports.is_empty()
};
let has_pending_dyn_module_evaluation =
!state.pending_dyn_mod_evaluate.is_empty();
let has_pending_module_evaluation = state.pending_mod_evaluate.is_some();
if !has_pending_ops
&& !has_pending_dyn_imports
&& !has_pending_dyn_module_evaluation
&& !has_pending_module_evaluation
{
return Poll::Ready(Ok(()));
}
// Check if more async ops have been dispatched
// during this turn of event loop.
if state.have_unpolled_ops.get() {
state.waker.wake();
}
if has_pending_module_evaluation {
if has_pending_ops
|| has_pending_dyn_imports
|| has_pending_dyn_module_evaluation
{
// pass, will be polled again
} else {
let msg = "Module evaluation is still pending but there are no pending ops or dynamic imports. This situation is often caused by unresolved promise.";
return Poll::Ready(Err(generic_error(msg)));
}
}
if has_pending_dyn_module_evaluation {
if has_pending_ops || has_pending_dyn_imports {
// pass, will be polled again
} else {
let msg = "Dynamically imported module evaluation is still pending but there are no pending ops. This situation is often caused by unresolved promise.";
return Poll::Ready(Err(generic_error(msg)));
}
}
Poll::Pending
}
}
A single tick of the event loop checks for a lot of things. As mentioned earlier, the only relevant piece wrt the hello world program is the function call: evaluate_pending_module.
Also, a module evaluation could result in errors. Evaluation errors would come asynchronously as promise rejections. These are the standard JS errors like ReferenceError, TypeError, etc.
Therefore in each tick of the event loop, the global promise is checked till it moves out of the pending state. It could move to resolved or rejected states.
Here is the code which checks for promises related to pending evaluations:
fn evaluate_pending_module(&mut self) {
let state_rc = Self::state(self.v8_isolate());
let context = self.global_context();
{
let scope =
&mut v8::HandleScope::with_context(self.v8_isolate(), context);
let mut state = state_rc.borrow_mut();
if let Some(module_evaluation) = state.pending_mod_evaluate.as_ref() {
let promise = module_evaluation.promise.get(scope);
let mut sender = module_evaluation.sender.clone();
let promise_state = promise.state();
match promise_state {
v8::PromiseState::Pending => {
// pass, poll_event_loop will decide if
// runtime would be woken soon
}
v8::PromiseState::Fulfilled => {
state.pending_mod_evaluate.take();
scope.perform_microtask_checkpoint();
sender.try_send(Ok(())).unwrap();
}
v8::PromiseState::Rejected => {
let exception = promise.result(scope);
state.pending_mod_evaluate.take();
drop(state);
scope.perform_microtask_checkpoint();
let err1 = exception_to_err_result::<()>(scope, exception, false)
.map_err(|err| attach_handle_to_error(scope, err, exception))
.unwrap_err();
sender.try_send(Err(err1)).unwrap();
}
}
}
};
}
There are three states of the global promise:
- Pending
- The promise is still pending
- Check again in the next tick
- Fulfilled
- Module evaluation has completed successfully
- Send success result to the receiver
- Rejected
- Module evaluation has failed
- Send error to the receiver

Once the global promise is out resolved/rejected, module evaluation is complete.
The event loop terminates when this condition is met:
- No pending ops
- No pending dynamic imports
- No dynamic imports pending evaluation
- No top-level modules pending evaluation
if !has_pending_ops
&& !has_pending_dyn_imports
&& !has_pending_dyn_module_evaluation
&& !has_pending_module_evaluation
{
return Poll::Ready(Ok(()));
}
In short, if there is nothing left to do, the event loop is done, and so is the program.
Let's see how the evaluation works in our example. To recall, this is our JS code:
"use strict";
function printNumber(input) {
console.log(input);
}
function printString(input) {
console.log(input);
}
printNumber(1);
printString('One');
Our example doesn't have any imports. The only module present in the root or main module. That's the only module that will get evaluated. There are no async calls or anything else that would keep it going.
When mod_evaluate is called for our main module, the following steps happen:
- V8 executes the main module
- V8 runs console.log
- V8 calls external reference print to print the number 1 on stdout
- V8 runs console.log
- V8 calls external reference print to print the word One on stdout
- The program is done as there is nothing else to do
- V8 finishes module evaluation
- V8 marks the module's global promise to fulfilled
- There is nothing else to do, nothing to wait for, etc.
- The next tick of the event loop happens
- The global promise has been fulfilled
- The asynchronous polling ends and execution of the module is done
Output:
1
One
While the execution of the module is done, there is still some more work left for the worker to do. Once the execute_module function finishes, it moves on to the next steps in the run command implementation:
worker.execute_module(&main_module).await?;
worker.execute("window.dispatchEvent(new Event('load'))")?;
worker.run_event_loop().await?;
worker.execute("window.dispatchEvent(new Event('unload'))")?;
Ok(())
All the remaining ones will finish immediately as the main module has already been completed. The program would finish as soon as the above 4 functions are done. Even the run_event_loop would finish immediately as there is nothing left to do.
Here are the remaining steps:
- The window load event is dispatched to listeners if there are any
- The event loop is entered
- But as pointed out earlier, the main module has already finished
- Event loop exits immediately as there is nothing else to do or wait for
- Window unload event is dispatched
- Program is done
- Deno quits