GopeedLab/gopeed · error
engine loop terminated
Error message
engine loop terminated
What it means
In Engine.runOnLoop (pkg/download/engine/engine.go:125-127), eventloop.RunOnLoop returns false when the JS event loop is no longer running — i.e. the loop was terminated. The engine then reports 'engine loop terminated'. Every engine API that marshals work onto the loop (RunString, and everything built on runOnLoop) fails with this once Engine.Close() (engine.go:135-137, loop.Terminate) has been called or the loop exited on its own.
Source
Thrown at pkg/download/engine/engine.go:126
return
}
onFulfilled := runtime.ToValue(func(call goja.FunctionCall) goja.Value {
sendResult(result{value: exportJSValue(call.Argument(0))})
return goja.Undefined()
})
onRejected := runtime.ToValue(func(call goja.FunctionCall) goja.Value {
sendResult(result{err: exportJSError(call.Argument(0))})
return goja.Undefined()
})
if _, err := thenFn(promiseObj, onFulfilled, onRejected); err != nil {
sendResult(result{err: err})
}
return
}
sendResult(result{value: exportJSValue(value)})
})
if !ok {
return nil, errors.New("engine loop terminated")
}
res := <-ch
if res.err != nil {
return nil, res.err
}
return res.value, nil
}
func (e *Engine) Close() {
e.loop.Terminate()
}
type Config struct {
ProxyConfig *base.DownloaderProxyConfig
StreamConfig *stream.Config
}
func NewEngine(cfg *Config) *Engine {View on GitHub (pinned to 7b7327ffb3)
Solutions
- Ensure no callers use the engine after Close: close it last, after all goroutines that reference it have stopped
- Track a closed flag (atomic.Bool) set before calling Close and check it at API entry points
- If the error surfaces, recreate the engine (NewEngine) and re-run the script — the old one is unrecoverable
- Order operations: stop producers/pollers -> drain -> Engine.Close()
Example fix
// before
e := engine.NewEngine(cfg)
// ... later, concurrently:
go e.Close()
_, err := e.RunString("1+1") // "engine loop terminated"
// after
type safeEngine struct { *engine.Engine; closed atomic.Bool }
func (s *safeEngine) Run(script string) (any, error) {
if s.closed.Load() { return nil, errors.New("engine closed") }
return s.Engine.RunString(script)
}
func (s *safeEngine) Close() { s.closed.Store(true); s.Engine.Close() } Defensive patterns
Strategy: try-catch
Validate before calling
var engineClosed atomic.Bool
run := func(script string) (any, error) {
if engineClosed.Load() { return nil, errors.New("engine closed") }
return engine.RunString(script)
}
closeEngine := func() { engineClosed.Store(true); engine.Close() } Try / catch
v, err := run(script)
if err != nil && strings.Contains(err.Error(), "engine loop terminated") {
// engine is dead: recreate with NewEngine and re-run once
} Prevention
- Check a closed flag before every engine API call
- Close the engine only after all users have stopped (join goroutines first)
- One owner goroutine for engine lifecycle; never close from arbitrary callbacks
When it happens
Trigger: Calling engine.RunString / any engine API after Engine.Close(); a concurrent Close from another goroutine (e.g. shutdown or error cleanup) while a script call is in flight; the underlying goja eventloop terminating due to unhandled termination inside script execution; using an engine stored in a pool that was drained and closed.
Common situations: Shutdown races: extension finishes and closes its engine while a stats/progress poller still queries it; reusing a closed engine because the close error was swallowed; per-request engines in servers being closed by a timeout path but still referenced by in-flight handlers.
Related errors
- engine loop terminated
- webview page is closed
- blob source not found
- blob source revoked
- blob source closed
AI-assisted analysis of GopeedLab/gopeed@7b7327ffb3 (2026-08-16).
Data as JSON: /api/errors/bafe1da948e6ec6d.
Report an issue: GitHub.