#550·otto

Support parallel execution of JS functions without global variables

Author: davedwwangCreated Jul 9, 2025Updated Jul 9, 2025

Error occurs when invoking the same JS function concurrently via multiple goroutines​. Here the code:

func IfJs() string {
	return `function callIf(i) {
	var g = i == 1;
    if (g) {
        i = i + 1;
    }
    return i;
}
`
}

func TestIfFuncJsParal(t *testing.T) {
	javascriptVM := New()
	_, err := javascriptVM.Run(IfJs())
	require.NoError(t, err)

	paral := 100
	wg := sync.WaitGroup{}
	wg.Add(paral)
	for i := 0; i < paral; i++ {
		go func() {
			defer wg.Done()
			f, err := javascriptVM.Get("callIf")
			require.NoError(t, err)
			_, err = f.Call(f, 1)
			require.NoError(t, err)
		}()
	}
	wg.Wait()
}

Here the ERROR:

fatal error: concurrent map writes

Description:​​

The root issue stems from how compiled callIf functions become nodeFunctionObject instances, each holding a pointer to a same runtime object (constructed via New()). When the Call method executes:

  1. The function scope is assigned to the runtime.scope field.
  2. Concurrent goroutines sharing the same runtime end up accessing:

The same scope The same stash The same underlying map

This creates unsafe concurrent map access when storing variables, triggering the observed race conditions.

Proposed Solutions

Option 1: ThreadLocal Isolation​

Pros: Explicit scope isolation Cons:

  • Introduces external dependencies
  • Anti-pattern in Go

Option 2: Context-Based Scope Passing​

  • Avoid shared scope by storing scope in context​
  • Built-in cancellation support via context

Request for Feedback​

Which approach would better align with otto’s design philosophy? Are there alternative patterns worth considering?