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

  1. Ensure no callers use the engine after Close: close it last, after all goroutines that reference it have stopped
  2. Track a closed flag (atomic.Bool) set before calling Close and check it at API entry points
  3. If the error surfaces, recreate the engine (NewEngine) and re-run the script — the old one is unrecoverable
  4. 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

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


AI-assisted analysis of GopeedLab/gopeed@7b7327ffb3 (2026-08-16). Data as JSON: /api/errors/bafe1da948e6ec6d. Report an issue: GitHub.