JavaScript
ES6
Python
Jupyter Notebook
Asynchronous Execution

In Javascript / ES6, how do I wait for Python code to finish executing in a Jupyter Notebook?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

If JavaScript needs to wait for Python running in a Jupyter kernel, the right model is asynchronous request tracking, not sleeping for an arbitrary amount of time. You send code to the kernel, keep the execution future or callback, and resolve a Promise when the kernel reports that execution finished.

The Core Idea: Wrap Kernel Execution in a Promise

In modern Jupyter front-end code, kernel execution is asynchronous. That means the JavaScript side should expose an async function and await it.

A JupyterLab-style example looks like this:

javascript
1async function runPython(kernel, code) {
2  const outputs = [];
3  const future = kernel.requestExecute({ code });
4
5  future.onIOPub = (msg) => {
6    if (msg.header.msg_type === "stream") {
7      outputs.push(msg.content.text);
8    }
9    if (msg.header.msg_type === "execute_result") {
10      outputs.push(msg.content.data["text/plain"]);
11    }
12  };
13
14  await future.done;
15  return outputs;
16}
17
18async function main(kernel) {
19  const result = await runPython(kernel, "x = 21 * 2\nprint(x)\nx");
20  console.log(result);
21}

The important part is await future.done. That is the moment JavaScript actually waits for Python execution to finish.

Why setTimeout Is the Wrong Tool

A common bad pattern is:

javascript
1runPythonSomehow();
2setTimeout(() => {
3  // assume Python finished
4}, 1000);

This is unreliable because kernel execution time depends on:

  • notebook load
  • current kernel state
  • data size
  • machine performance
  • remote latency

If Python finishes sooner, you waited too long. If Python finishes later, your JavaScript races ahead anyway.

Promises and kernel futures solve the real synchronization problem instead of guessing.

Handling Errors Explicitly

You usually want the Promise to reject if the kernel execution fails:

javascript
1async function runPython(kernel, code) {
2  const future = kernel.requestExecute({ code });
3
4  future.onReply = (msg) => {
5    if (msg.content.status === "error") {
6      throw new Error(msg.content.evalue);
7    }
8  };
9
10  await future.done;
11}

A more complete version would capture the error and reject a Promise cleanly, but the key idea is that kernel status should drive the control flow.

This matters a lot in notebooks because silent failures are common if you only watch output areas and never check the execution reply.

Legacy Notebook Pattern

Older classic Notebook front-ends often used globals such as Jupyter.notebook.kernel.execute. The same async principle still applies: wrap the completion callback in a Promise and resolve it when the kernel replies.

Conceptually, it looks like this:

javascript
1function runPythonLegacy(code) {
2  return new Promise((resolve, reject) => {
3    Jupyter.notebook.kernel.execute(code, {
4      shell: {
5        reply: (msg) => {
6          if (msg.content.status === "ok") {
7            resolve(msg);
8          } else {
9            reject(new Error(msg.content.evalue || "Execution failed"));
10          }
11        }
12      }
13    });
14  });
15}

Then:

javascript
await runPythonLegacy("print('hello from python')");

The exact API surface depends on the notebook front-end, but the design pattern is the same in both classic and modern environments.

Keep JavaScript and Python Responsibilities Clear

A lot of notebook code becomes brittle because both languages try to manage the same workflow state. It is usually cleaner to let:

  • Python do the heavy computation
  • JavaScript handle UI behavior and await kernel completion

That separation makes async coordination much easier than bouncing state back and forth through notebook output cells.

Returning Data Instead of Just Waiting

Waiting is only half the story. Most real use cases also need the result. In modern Jupyter front-end APIs, the execution future lets you collect stream messages, rich display data, and execute results while still awaiting completion.

That means one Promise can do all three jobs:

  • start Python execution
  • gather outputs
  • resolve only when the kernel is finished

Once you think of the kernel request as an async task with a completion handle, the JavaScript side becomes straightforward.

Common Pitfalls

  • Using setTimeout to guess when Python finished.
  • Starting a kernel execution and ignoring the completion future or reply callback.
  • Treating notebook output rendering as proof that execution fully completed.
  • Mixing classic notebook globals with modern JupyterLab APIs without checking which front-end you are actually running.
  • Ignoring kernel error replies and then wondering why later JavaScript state is wrong.

Summary

  • JavaScript should wait for Python in Jupyter by awaiting the kernel execution future, not by sleeping.
  • In modern front-ends, requestExecute plus await future.done is the core pattern.
  • In classic notebook code, wrap the execution callback in a Promise and await that Promise.
  • The same async model can both wait for completion and collect outputs.
  • Reliable notebook integration comes from explicit kernel completion handling, not time-based guesses.

Course illustration
Course illustration

All Rights Reserved.