From ca3b6ea2dc10d2820692a7f7daad8ebda6f644cd Mon Sep 17 00:00:00 2001 From: Jeremy Tuloup Date: Fri, 6 Mar 2026 18:08:21 +0100 Subject: [PATCH 1/2] Improve error handling --- packages/javascript-kernel/src/executor.ts | 48 +++++++++++--- .../src/runtime_evaluator.ts | 63 +++++++++++++------ 2 files changed, 81 insertions(+), 30 deletions(-) diff --git a/packages/javascript-kernel/src/executor.ts b/packages/javascript-kernel/src/executor.ts index 6cd1efb..59bad1a 100644 --- a/packages/javascript-kernel/src/executor.ts +++ b/packages/javascript-kernel/src/executor.ts @@ -768,23 +768,51 @@ export class JavaScriptExecutor { * @returns The cleaned stack trace string. */ cleanStackTrace(error: Error): string { - const errStackStr = error.stack || ''; - const errStackLines = errStackStr.split('\n'); - const usedLines: string[] = []; + const header = `${error.name}: ${error.message}`; + const stack = error.stack || ''; + const lines = stack.split('\n'); + const userFrames: string[] = []; + + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed) { + continue; + } + + // Some browsers repeat `Name: message` as the first stack line. + if (trimmed.startsWith(`${error.name}:`)) { + continue; + } - for (const line of errStackLines) { - // Stop at internal implementation details + // Stop once we reach internal executor frames. if ( - line.includes('makeAsyncFromCode') || - line.includes('new Function') || - line.includes('asyncFunction') + trimmed.includes('makeAsyncFromCode') || + trimmed.includes('new Function') || + trimmed.includes('asyncFunction') ) { break; } - usedLines.push(line); + + // Keep eval frames from user code. + if (/\beval\b/.test(trimmed) || trimmed.includes('')) { + userFrames.push(line); + continue; + } + + // Drop stack frames that point to bundled code. + if (/^\s*at\s+/.test(trimmed) || /^[^@]*@\S+:\d+:\d+$/.test(trimmed)) { + continue; + } + + // Keep any other lines (may be useful context). + userFrames.push(line); + } + + if (userFrames.length > 0) { + return `${header}\n${userFrames.join('\n')}`; } - return usedLines.join('\n'); + return header; } /** diff --git a/packages/javascript-kernel/src/runtime_evaluator.ts b/packages/javascript-kernel/src/runtime_evaluator.ts index 7492625..d77c917 100644 --- a/packages/javascript-kernel/src/runtime_evaluator.ts +++ b/packages/javascript-kernel/src/runtime_evaluator.ts @@ -53,10 +53,20 @@ export class JavaScriptRuntimeEvaluator { code: string, executionCount: number ): Promise { + // Parse-time errors are syntax errors, so show only `Name: message`. + let asyncFunction: () => Promise; + let withReturn: boolean; try { - const { asyncFunction, withReturn } = - this._executor.makeAsyncFromCode(code); + const parsed = this._executor.makeAsyncFromCode(code); + asyncFunction = parsed.asyncFunction; + withReturn = parsed.withReturn; + } catch (error) { + const normalized = normalizeError(error); + return this._emitError(executionCount, normalized, false); + } + // Runtime errors may include useful eval frames from user code. + try { const resultPromise = this._evalFunc(asyncFunction); if (withReturn) { @@ -84,24 +94,7 @@ export class JavaScriptRuntimeEvaluator { }; } catch (error) { const normalized = normalizeError(error); - const cleanedStack = this._executor.cleanStackTrace(normalized); - - const content: KernelMessage.IReplyErrorContent = { - status: 'error', - ename: normalized.name || 'Error', - evalue: normalized.message || '', - traceback: [cleanedStack] - }; - - this._onOutput({ - type: 'execute_error', - bundle: content - }); - - return { - ...content, - execution_count: executionCount - }; + return this._emitError(executionCount, normalized, true); } } @@ -148,6 +141,36 @@ export class JavaScriptRuntimeEvaluator { return asyncFunc.call(this._globalScope); } + /** + * Build and emit an execute error reply. + */ + private _emitError( + executionCount: number, + error: Error, + includeStack: boolean + ): KernelMessage.IExecuteReplyMsg['content'] { + const traceback = includeStack + ? this._executor.cleanStackTrace(error) + : `${error.name}: ${error.message}`; + + const content: KernelMessage.IReplyErrorContent = { + status: 'error', + ename: error.name || 'Error', + evalue: error.message || '', + traceback: [traceback] + }; + + this._onOutput({ + type: 'execute_error', + bundle: content + }); + + return { + ...content, + execution_count: executionCount + }; + } + /** * Patch console methods in runtime scope to emit Jupyter stream messages. */ From 134002c2fcd82d620b1dc3183c40c76539267328 Mon Sep 17 00:00:00 2001 From: Jeremy Tuloup Date: Sat, 7 Mar 2026 09:17:07 +0100 Subject: [PATCH 2/2] simplify --- packages/javascript-kernel/src/executor.ts | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/packages/javascript-kernel/src/executor.ts b/packages/javascript-kernel/src/executor.ts index 59bad1a..0919aa7 100644 --- a/packages/javascript-kernel/src/executor.ts +++ b/packages/javascript-kernel/src/executor.ts @@ -10,6 +10,9 @@ import type { IMimeBundle } from '@jupyterlab/nbformat'; export { IDisplayData, IDisplayCallbacks, DisplayHelper } from './display'; +/** Matches the word "eval" in a stack frame (user eval code). */ +const RE_EVAL = /\beval\b/; + /** * Configuration for magic imports. */ @@ -793,19 +796,10 @@ export class JavaScriptExecutor { break; } - // Keep eval frames from user code. - if (/\beval\b/.test(trimmed) || trimmed.includes('')) { + // Only keep lines that reference user eval code. + if (RE_EVAL.test(trimmed) || trimmed.includes('')) { userFrames.push(line); - continue; - } - - // Drop stack frames that point to bundled code. - if (/^\s*at\s+/.test(trimmed) || /^[^@]*@\S+:\d+:\d+$/.test(trimmed)) { - continue; } - - // Keep any other lines (may be useful context). - userFrames.push(line); } if (userFrames.length > 0) {