projectdiscovery/katana · warning
ErrElementNotVisible
ErrElementNotVisible
Error message
element not visible
What it means
ErrElementNotVisible is returned by dispatchCrawlAction when the DOM element targeted by a crawl action cannot be made visible/interactable in the headless browser. Crawl counts it as a consecutive failure and skips the action instead of aborting the whole crawl.
Source
Thrown at pkg/engine/headless/crawler/crawler.go:484
return err
}
}
err = c.crawlGraph.AddPageState(*pageState)
if err != nil {
return err
}
// TODO: Check if the page opened new sub pages and if so capture their
// navigation as well as close them so the state change can work.
if len(navigations) == 0 && c.crawlQueue.Size() == 0 {
return ErrNoCrawlingAction
}
return nil
}
var ErrElementNotVisible = errors.New("element not visible")
func (c *Crawler) executeCrawlStateAction(action *types.Action, page *browser.BrowserPage) error {
return runWithActionHooks(c.options.Hooks, page, action, func() error {
return c.dispatchCrawlAction(action, page)
})
}
func (c *Crawler) dispatchCrawlAction(action *types.Action, page *browser.BrowserPage) error {
var err error
switch action.Type {
case types.ActionTypeLoadURL:
// Apply a timeout to every critical Rod call.
pTimeout := page.Timeout(c.options.PageMaxTimeout)
if err := pTimeout.Navigate(action.Input); err != nil {
return err
}
if err = page.WaitPageLoadHeurisitics(); err != nil {View on GitHub (pinned to e3e742739c)
Solutions
- Let the crawler skip it — it increments consecutiveFailures and continues automatically
- Re-run with slower pacing / wait options so dynamic UI has time to become visible
- Audit the target page for overlays (cookie banners) and pre-dismiss via custom hooks
- Update or loosen selectors in custom actions so they target always-visible elements
Example fix
// before: strict selector hitting hidden element action.Selector = "#submit-button" // display:none until form filled // after: trigger the visibility first action.Selector = "form.open #submit-button" // wait for state where button is visible
Defensive patterns
Strategy: retry
Validate before calling
// Before executing, check the element exists and is interactable:
el, err := page.Element(action.Selector)
if err != nil || el == nil { skip } Type guard
func isElementNotVisible(err error) bool { return errors.Is(err, crawler.ErrElementNotVisible) } Try / catch
if errors.Is(err, crawler.ErrElementNotVisible) {
consecutiveFailures++
time.Sleep(backoff)
continue // retry later or skip action
} Prevention
- Add wait/retry pacing so dynamic UI becomes visible before actions run
- Pre-dismiss overlays (cookie banners) via hooks
- Prefer selectors on stable, always-visible elements
- Cap consecutiveFailures to avoid infinite retry loops
When it happens
Trigger: executeCrawlStateAction → dispatchCrawlAction finds an element matched by the action's selector but its visibility check fails (crawler.go:528: if !visible return ErrElementNotVisible), e.g. element hidden behind CSS (display:none), off-viewport, or covered by overlays.
Common situations: Crawling SPAs with lazy-loaded or modal-gated UI; consent banners covering interactive elements; animations delaying visibility when the action runs; stale selectors after page mutations.
Related errors
- ErrNoCrawlingAction
- ErrEmptyPage
- ErrNoNavigationPossible
- could not create new page
- could not initialize stealth
AI-assisted analysis of projectdiscovery/katana@e3e742739c (2026-09-03).
Data as JSON: /api/errors/89eae6a6f4e79ac5.
Report an issue: GitHub.