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 writesDescription:
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:
- The function scope is assigned to the runtime.scope field.
- 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?
Source: robertkrimen/otto