{"record":{"id":"78b85c5f93504d0e","repo":"projectdiscovery/nuclei","slug":"nuclei-js-runtime-program-failed-to-terminate-aft-78b85c","errorCode":null,"errorMessage":"nuclei js runtime: program failed to terminate after interrupt","messagePattern":"nuclei js runtime: program failed to terminate after interrupt","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/js/compiler/session.go","lineNumber":180,"sourceCode":"\t\ts.config.runtime.Interrupt(contextErr)\n\n\t\ttimer := time.NewTimer(time.Second)\n\t\tdefer timer.Stop()\n\n\t\tselect {\n\t\tcase result := <-s.resultChan:\n\t\t\treturn result, nil\n\t\tcase <-timer.C:\n\t\t\treturn gojaRunResult{}, s.abandon(contextErr)\n\t\t}\n\tcase result := <-s.resultChan:\n\t\treturn result, nil\n\t}\n}\n\nfunc (s *session) abandon(contextErr error) error {\n\ts.state = sessionAbandoned\n\terr := fmt.Errorf(\"%w: %w\", errRuntimeTerminationTimeout, contextErr)\n\tif s.config.releaseAbandonedSlot != nil {\n\t\tresultChan := s.resultChan\n\t\treleaseAbandonedSlot := s.config.releaseAbandonedSlot\n\t\tgo func() {\n\t\t\t<-resultChan\n\t\t\treleaseAbandonedSlot()\n\t\t}()\n\t}\n\tif s.config.onAbandon != nil {\n\t\ts.config.onAbandon(err)\n\t}\n\treturn err\n}\n\nfunc (s *session) cleanupCommon() {\n\tif s.state == sessionAbandoned || !s.commonPrepared || s.commonCleaned {\n\t\treturn\n\t}","sourceCodeStart":162,"sourceCodeEnd":198,"githubUrl":"https://github.com/projectdiscovery/nuclei/blob/265b3a3dec374741614e342f813c10f8b38d2bb7/pkg/js/compiler/session.go#L162-L198","documentation":"The template's context deadline fired, goja was interrupted, but the JS program still did not terminate before the grace timer expired, so the session is abandoned (state=sessionAbandoned). The error joins errRuntimeTerminationTimeout ('nuclei js runtime: ...') with the context error. The concurrency slot is only released later, by the goroutine waiting on resultChan after the program eventually finishes.","triggerScenarios":"Tight CPU-bound loops in template JS (while(true){}, huge for-loops) that swallow goja's interrupt checks; long native calls that ignore interruption; a -timeout/-js-timeout budget smaller than the script's real runtime.","commonSituations":"Code-protocol templates with unbounded iteration; brute-force style JS loops over big wordlists inside the script instead of using nuclei' native flows; overloaded machines making valid scripts exceed the deadline repeatedly.","solutions":["Bound loops in the template's JS and break work into smaller steps so interrupts can land","Move heavy iteration to native nuclei features (payloads, workflows) instead of a JS loop","Raise the timeout budget for that template if the work is legitimately long","Expect resource lag: abandoned sessions hold their slot until the goroutine finally exits — reduce concurrency pressure after seeing these"],"exampleFix":"// before (template code block)\nwhile (true) { compute(next()); }\n\n// after\nfor (let i = 0; i < maxItems; i++) { compute(items[i]); }","handlingStrategy":"try-catch","validationCode":"// in template JS: make loops interruptible and bounded\nconst MAX = 10000;\nfor (let i = 0; i < items.length && i < MAX; i++) { work(items[i]); }","typeGuard":null,"tryCatchPattern":"Treat errors.Is(err, errRuntimeTerminationTimeout) (message contains 'failed to terminate after interrupt') as a hung template: stop scheduling it, record the template ID, and continue other work — retrying immediately will hang again. Account for the abandoned session holding its slot until the goroutine exits.","preventionTips":["Never write unbounded loops in code-protocol templates","Set realistic -timeout / JS timeouts relative to the script's work size","Prefer native nuclei payloads/flows over heavy JS iteration"],"tags":["javascript","goja","timeout","hang","runtime"],"backgroundTag":null,"analyzedSha":"265b3a3dec374741614e342f813c10f8b38d2bb7","analyzedAt":"2026-08-15T20:05:51.855Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}