Skip to content

VM: recovered panics are reported as successful runs (silent exit 0) #44

Description

@mparrett

A Go panic anywhere inside the interpreter loop is recovered, discarded, and reported to the caller as a successful run. The process prints nothing further and exits 0.

Mechanism

VM.run installs a blanket recover (pkg/vm/vm.go:1250-1259):

func (vm *VM) run() (InterpretResult, Value) {
	defer func() {
		if r := recover(); r != nil {
			if debugVM {
				fmt.Printf("[PANIC] Recovered panic in VM.run(): %v\n", r)
				debug.PrintStack()
			}
		}
	}()

debugVM is false in normal builds, so the panic value and stack are dropped on the floor. Because the returns are unnamed, the recovered function then yields the zero values — and InterpretOK is the first iota in the InterpretResult block (pkg/vm/vm.go:389-395), so zero is InterpretOK.

The caller therefore cannot distinguish a crash from a clean run: it sees InterpretOK and an undefined result.

Reproduction

Any panicking native will do. Before the BigInt fix, this was a one-liner:

var b = 10n;
console.log("before");
console.log(b.toString());   // panics inside the native
console.log("after");
$ ./paserati --no-typecheck repro.js
before
$ echo $?
0

No error, no stack, no non-zero exit — after simply never runs.

Why it matters

  • It hides bugs for as long as nobody looks. The BigInt breakage meant every BigInt.prototype method was unusable on a primitive receiver, and it presented as "the script stops early."
  • It is invisible to a test harness. Anything checking exit status or absence of stderr sees success. A panicking script looks like a passing one.
  • debugVM is the wrong gate. It has to be flipped at compile time and turns on very heavy tracing, so nobody enables it just to find out why a script stopped.

Possible directions

Deliberately not proposing a patch, since the right answer is a design call:

  1. Convert the recovered panic into a runtime error — return InterpretRuntimeError with the panic value as the message, so it surfaces through the normal error path and sets a non-zero exit.
  2. Re-panic after logging, so it fails loudly with a usable stack.
  3. Keep recovering, but always write the panic value and stack to stderr regardless of debugVM, and return InterpretRuntimeError.

Any of the three would have made the BigInt bug obvious immediately. Option 1 seems most in keeping with the engine's existing runtimeError handling, but it is your call.

Related: the BigInt bug that surfaced this.

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions