5.8 JS Runtime
JS runtime is the core JS execution handler within Deno. It isn't really a JS engine, rather an interface to Deno and to the v8. JS runtime uses rusty_v8 to communicate with the v8 engine. JS Runtime provides a lot of functionality starting from module loading, instantiation, execution to the famous event loop. JS runtime has a lot of v8 interfacing code.
For any user-level program, JS runtime provides a future that gets finished in the following three conditions:
- An error is encountered
- All the modules have been evaluated
- All pending ops are completed
If neither of the above happens, the future continues to get polled in the event loop. In our hello world example, we'll see that the future ends very quickly because there is no waiting operation.
JS runtime does a lot of work and therefore is quite complex. We'll go through a high-level view of JS Runtime here. We'll see more as when we see when scripts are executed. JS runtime is where a lot of action happens.
The initialization of JS runtime happens in a number of steps. Let's look at it briefly.

The first step is to initialize v8.
- Create a new default platform in v8
- Initialize platform
- Set v8 flags as passed from the command line
pub unsafe fn v8_init() {
let platform = v8::new_default_platform().unwrap();
v8::V8::initialize_platform(platform);
v8::V8::initialize();
let argv = vec![
"".to_string(),
"--wasm-test-streaming".to_string(),
"--no-wasm-async-compilation".to_string(),
"--harmony-top-level-await".to_string(),
];
v8::V8::set_flags_from_command_line(argv);
}
In v8 terminology, there are contexts for scripts or modules. However, there is also a global context. This global context gets created at the time of initialization of v8. The global context is used in many places like module loading, instantiation, etc.
Creating a global context happens through multiple steps, starting with the creation of isolates. Isolates come from v8. For more information on isolates, see v8.dev. Isolates are just initialized here:
let isolate = v8::Isolate::new(params);
let mut isolate = JsRuntime::setup_isolate(isolate);
{
let scope = &mut v8::HandleScope::new(&mut isolate);
let context = if snapshot_loaded {
v8::Context::new(scope)
} else {
// If no snapshot is provided, we initialize the context with empty
// main source code and source maps.
bindings::initialize_context(scope)
};
global_context = v8::Global::new(scope, context);
}
(isolate, None)
};
JsRuntime needs a complicated state which it maintains in v8's slots. The state consists of a number of items, such as:
isolate.set_slot(Rc::new(RefCell::new(JsRuntimeState {
global_context: Some(global_context),
pending_promise_exceptions: HashMap::new(),
pending_dyn_mod_evaluate: HashMap::new(),
pending_mod_evaluate: None,
shared_ab: None,
js_recv_cb: None,
js_macrotask_cb: None,
js_error_create_fn,
shared: SharedQueue::new(RECOMMENDED_SIZE),
pending_ops: FuturesUnordered::new(),
pending_unref_ops: FuturesUnordered::new(),
op_state: Rc::new(RefCell::new(op_state)),
have_unpolled_ops: Cell::new(false),
modules: Modules::new(),
loader,
dyn_import_map: HashMap::new(),
preparing_dyn_imports: FuturesUnordered::new(),
pending_dyn_imports: FuturesUnordered::new(),
waker: AtomicWaker::new(),
})));
There are a lot of states maintained by Deno to perform its operation. We'll see some of them as we go deep in running our code.
That was all about initialization. Let's see how JsRuntime executes programs. We'll stay at a high-level.
JsRuntime provides a function called execute() which is used to execute a script in the v8. Remember, Deno is a runtime, not a JS execution engine. All the script execution happens in v8.
The execute function is specifically used to execute traditional JS code, i.e. the one without ES modules. There are separate functions that can be used to run a module-based JS code. The execute function runs in the global context. As seen in the previous section (main worker), the execute function is used to bootstrap the runtime. It is appropriate to invoke this function at the level of global context, and not at the level of the user program.
pub fn execute(
&mut self,
js_filename: &str,
js_source: &str,
) -> Result<(), AnyError> {
self.shared_init();
let context = self.global_context();
let scope = &mut v8::HandleScope::with_context(self.v8_isolate(), context);
let source = v8::String::new(scope, js_source).unwrap();
let name = v8::String::new(scope, js_filename).unwrap();
let origin = bindings::script_origin(scope, name);
let tc_scope = &mut v8::TryCatch::new(scope);
let script = match v8::Script::compile(tc_scope, source, Some(&origin)) {
Some(script) => script,
None => {
let exception = tc_scope.exception().unwrap();
return exception_to_err_result(tc_scope, exception, false);
}
};
match script.run(tc_scope) {
Some(_) => Ok(()),
None => {
assert!(tc_scope.has_caught());
let exception = tc_scope.exception().unwrap();
exception_to_err_result(tc_scope, exception, false)
}
}
}
Execution of the script happens in a number of important steps:
- Get the global context
- Initialize script or file
- Allocate v8's TryCatch error handler
- Compile script
- Run script
One of the major functionality of JS runtime is to support and handle ES modules. ES modules require special handling compared to an old-style JS code. JS runtime provides this functionality through several important functions.

There are a lot of functions within the JS runtime that are used to support ES modules. Here is a quick description of some of the important ones:
Function | Use |
mod_new |
|
mod_instantiate |
|
dyn_mod_evaluate |
|
mod_evaluate |
|
load_module |
|
There are a lot more functions that are used to support ES modules, but it would be way too much to discuss them.
JS runtime also runs the famous event loop. The event loop has been made famous from the JS world, especially from Node.js. The event loop is a magical loop that runs till a program is done.
The event loop is basically a future and it keeps running till there is nothing left to do. Here is the base code of the 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
}
}
The event loop is a bit tough to understand without going through the actual code. We'll see the event loop when we go through the evaluation of the main module for the hello world program. For now, let's see what the event loop does at a high level.
poll_event_loop is an async function that will keep running till:
- There is an error, or
- There is nothing left to do
- There are no pending ops
- There are no pending dynamic imports
- There are no dynamic imports pending evaluation
- There are no modules pending evaluation
If there is nothing to do, the event loop ends. In our example, the event loop would end quickly. However, in practical applications, there would never be an end. For example, a web server would always be waiting for new connections. Production applications would mostly end when there is an error.
Here are some high-level tasks carried out by the poll event loop:
- Poll pending ops
- Check async ops response
- Drain macrotasks
- Check promise exceptions
- Prepare dynamic module imports
- Poll dynamic imports
- Evaluate dynamic module imports
- Check promise exceptions
- Evaluate pending top-level module
- Check exceptions
- etc. ....
We'll see parts of the event loop as we go further in our program in this and the next chapter. There is no way to understand the event loop without a context or example.
That's all about JS runtime. So far we've covered how the initialization happens. That was more about preparation. Now let's get into action which is running the user-level code of our hello world program. The next section is about executing the main module. Let's see how our code gets loaded and executed.