Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

“null is not an object” means your code tried to use a property or method on a value that is null. In PhantomJS, the usual trigger is a selector that matched nothing, followed immediately by a call such as .getBoundingClientRect(). Fix it by checking page.open status, querying and testing the element inside page.evaluate, validating the selector against the live DOM, and waiting for a deterministic readiness condition on dynamic pages.

This guide shows defensive PhantomJS code, selector and frame diagnostics, asynchronous waiting patterns, and a troubleshooting workflow that avoids masking the real cause with arbitrary delays.

What PhantomJS is telling you

document.querySelector(selector) returns null when no element matches. Dereferencing that result produces the TypeError reported as “null is not an object.” The error identifies the failed value, not necessarily the root cause: the element may be absent because the selector is wrong, the page failed to load, JavaScript has not rendered the element yet, or the element belongs to another frame.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

A typical failing expression is:

var box = page.evaluate(function () {
  return document.querySelector('#map').getBoundingClientRect();
});

If #map is not present at the instant the callback runs, querySelector('#map') is null and getBoundingClientRect() cannot be called. The same principle applies to any property or method access: node.textContent, node.click(), node.style, and so on.

Use this order to isolate the cause

  1. Verify navigation. Run DOM code only after page.open calls its callback and confirm the status is 'success'.
  2. Check the lookup in page context. Keep querySelector and the null test in one page.evaluate call.
  3. Prove the selector. Compare its spelling, punctuation, and whitespace with the current markup.
  4. Wait for a condition, not a guess. Poll for the required element or state when a framework renders content asynchronously.
  5. Confirm document and frame. Log page.url; switch to the correct iframe before querying content inside it.
  6. Record evidence. Capture the URL, selector, ready state, and a short markup excerpt before changing the script.

Check page.open before touching the DOM

PhantomJS supplies the load callback with 'success' or 'fail'. A failed navigation can leave you querying an empty or unexpected document, so stop early and return a useful exit code.

var page = require('webpage').create();
var system = require('system');
var url = system.args[1];

page.open(url, function (status) {
  if (status !== 'success') {
    console.log('Unable to load: ' + url + ' (status: ' + status + ')');
    phantom.exit(1);
    return;
  }

  console.log('Loaded ' + page.url);
  // DOM work belongs here, after the status check.
});

Do not treat a successful network load as proof that a single-page application has finished rendering. It only establishes that PhantomJS completed the navigation step.

Perform a null-safe query inside page.evaluate

page.evaluate runs in the page’s sandbox. Query the element, test it, and return plain data that PhantomJS can serialize. Returning a DOM node itself, or relying on variables from the outer script, does not work across this boundary.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
var result = page.evaluate(function (selector) {
  var element = document.querySelector(selector);
  if (!element) {
    return {
      found: false,
      readyState: document.readyState,
      text: ''
    };
  }

  return {
    found: true,
    readyState: document.readyState,
    text: element.textContent || ''
  };
}, '#map');

if (!result.found) {
  console.log('Selector #map was not found; readyState=' + result.readyState);
  phantom.exit(2);
  return;
}

console.log(result.text);

The lookup and the test are deliberately adjacent. That prevents a race in which one call checks the element and a later call assumes it still exists. Return strings, numbers, booleans, arrays, and plain objects; keep DOM manipulation inside the evaluated function.

Validate the selector against the actual markup

A one-character selector mistake is enough to produce null. Check the tag name, id, class, attribute spelling, quoting, and combinators. Whitespace can change the meaning completely:

Rank #2
Sale
// Descendant selector: an img inside an element with an alt attribute.
'img [alt="PhantomJS"]'

// Attribute selector: an img whose own alt attribute matches.
'img[alt="PhantomJS"]'

The first selector looks similar but requires an ancestor between img and the attribute-bearing element. If the image itself owns the attribute, it matches nothing.

When the selector is uncertain, inspect the document that PhantomJS actually received:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
var html = page.content;
console.log(html.substring(0, 1200));

For a more focused check, return a small diagnostic object rather than dumping an entire page:

var diagnosis = page.evaluate(function (selector) {
  return {
    selector: selector,
    found: !!document.querySelector(selector),
    readyState: document.readyState,
    title: document.title,
    bodyLength: document.body ? document.body.innerHTML.length : 0
  };
}, '#map');

console.log(JSON.stringify(diagnosis));

Wait for dynamically rendered elements

Client-side frameworks can add the target after the load callback. Replace a fixed sleep with a condition that expresses what “ready” means for your task. The following helper polls in the PhantomJS script until a selector exists or a timeout expires.

function waitForSelector(page, selector, timeout, interval, done) {
  var started = Date.now();
  var timer = setInterval(function () {
    var state = page.evaluate(function (sel) {
      var node = document.querySelector(sel);
      return {
        found: !!node,
        readyState: document.readyState
      };
    }, selector);

    if (state.found) {
      clearInterval(timer);
      done(null, state);
      return;
    }

    if (Date.now() - started >= timeout) {
      clearInterval(timer);
      done(new Error('Timed out waiting for ' + selector), state);
    }
  }, interval);
}

page.open(url, function (status) {
  if (status !== 'success') {
    console.log('Load failed: ' + status);
    phantom.exit(1);
    return;
  }

  waitForSelector(page, '#map', 10000, 100, function (error, state) {
    if (error) {
      console.log(error.message + '; readyState=' + state.readyState);
      console.log(page.content.substring(0, 1200));
      phantom.exit(2);
      return;
    }

    var box = page.evaluate(function () {
      return document.querySelector('#map').getBoundingClientRect();
    });
    console.log(JSON.stringify({ left: box.left, top: box.top,
      width: box.width, height: box.height }));
    phantom.exit(0);
  });
});

Choose a timeout that fits the page you control and make the timeout visible in logs. A long delay can hide a broken selector; a short one can fail a legitimately slow render. If you need delayed, non-blocking work in page context, PhantomJS also provides evaluateAsync(function, delayMillis, ...). Use it for a known page-side delay, but still verify the resulting state instead of assuming the delay succeeded.

Check frames and navigation changes

Content inside an iframe

A selector searches the current document only. If the target is inside an iframe, switch to that frame before evaluating:

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
var frameIndex = page.evaluate(function () {
  var frames = document.querySelectorAll('iframe');
  for (var i = 0; i < frames.length; i++) {
    if (frames[i].id === 'map-frame') return i;
  }
  return -1;
});

if (frameIndex < 0) {
  console.log('map-frame was not found');
  phantom.exit(2);
  return;
}

page.switchToFrame(frameIndex);
var inside = page.evaluate(function () {
  var node = document.querySelector('#map');
  return { found: !!node, text: node ? node.textContent : '' };
});
page.switchToMainFrame();

Frame indexes can change when the page structure changes. If possible, identify the frame by a stable name or id and log which one you selected.

Unexpected navigation

Redirects, login pages, and client-side route changes can replace the document after the initial request. Log page.url immediately before the query and compare it with the URL you intended to open. If it differs, inspect page.content and adjust authentication, redirects, or the selector for the actual page.

Instrument failures instead of guessing

Page-side console messages are not printed by default. Attach a handler while diagnosing:

page.onConsoleMessage = function (message) {
  console.log('PAGE: ' + message);
};

page.open(url, function (status) {
  console.log(JSON.stringify({
    requested: url,
    status: status,
    current: page.url,
    readyState: page.evaluate(function () { return document.readyState; })
  }));

  if (status !== 'success') {
    phantom.exit(1);
    return;
  }

  var check = page.evaluate(function (selector) {
    var node = document.querySelector(selector);
    return {
      found: !!node,
      readyState: document.readyState,
      text: node ? (node.textContent || '') : ''
    };
  }, '#map');

  if (!check.found) {
    console.log('Missing selector; markup preview:');
    console.log(page.content.substring(0, 1200));
    phantom.exit(2);
    return;
  }

  console.log(check.text);
  phantom.exit(0);
});

Keep the selector, URL, status, ready state, and a bounded markup excerpt in failure logs. This makes a CI failure reproducible without flooding logs with an entire page.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Common symptoms and precise fixes

Symptom Likely cause Fix
Error occurs immediately in the open callback Selector does not match initial markup Run a null-safe query, inspect page.content, and correct tag, id, class, attribute, or whitespace.
Load status is fail Navigation, DNS, TLS, or server failure Log status and URL, stop DOM work, and investigate the page load before changing selectors.
Selector works in a browser but not PhantomJS Different markup, route, user state, or unsupported page behavior Compare PhantomJS’s page.url and page.content with the expected document.
Element appears after a visible delay Asynchronous rendering Poll for the element or a framework-specific ready marker; set and report a finite timeout.
Outer page contains the iframe but target is missing Query runs in the wrong document Switch to the appropriate frame, query, then return to the main frame.
Value returned from evaluate is empty or unusable A DOM node, closure, or other non-serializable object crossed the sandbox boundary Extract primitive fields or a plain JSON object inside evaluate.
Adding a larger sleep changes the result unpredictably Timing is nondeterministic or the selector is wrong Replace the sleep with a condition and retain timeout diagnostics.

Performance and reliability choices

Prefer state checks over fixed delays

A condition-based poll returns as soon as the element exists and fails promptly when it never will. Fixed delays make every successful run slower and still do not prove that the page is ready.

Keep evaluated work small

Each page.evaluate call crosses the page boundary. Query once, extract the fields you need, and return a compact object. Do not repeatedly serialize large markup unless a failure requires a preview.

Make retries safe

Retry navigation only when the failure is transient and cap the number of attempts. A retry cannot repair a misspelled selector, wrong frame, or page that consistently renders a different route. Include the attempt number and final URL in logs.

Use explicit exit codes

For automation, reserve separate codes for load failure, missing selector, and success. Your CI system can then distinguish an unavailable page from a script defect without parsing prose logs.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Or skip the browser setup:

If your goal is a clean screenshot or PDF rather than PhantomJS-specific DOM debugging, ScreenshotNeo provides a single HTTP request. It accepts the cookie or consent banner like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the page verdict and billing status in X-Page-Verdict and X-Billed headers. It also offers an MCP server for AI agents, with take_screenshot, get_page_info, and capture_pdf tools.

cURL (see the ScreenshotNeo API documentation):

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

Python:

import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)

Node.js:

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot failed: ${res.status}`);
const data = Buffer.from(await res.arrayBuffer());
require('fs').writeFileSync('shot.webp', data);

ScreenshotNeo includes full-page captures with lazy images loaded, CSS-selector element shots, dark mode, 12 device presets plus custom viewports, retina scale, PDF paper and page controls, custom CSS and JavaScript, clicks before capture, selector waits, delays or network-idle waits, request and resource blocking, custom headers, cookies, user agents, authorization, timezone and geolocation, transparent backgrounds, resizing, selectable cache TTLs, signed links, asynchronous jobs with signed webhooks, bulk capture for up to 100 URLs per call, a usage API, an OpenAPI specification, and compatibility with parameter names used by other screenshot APIs. Every feature is included on every plan. The Free plan includes 1,000 shots per month with no card; paid plans start at $5 for 3,000 shots, with yearly billing providing two months free.

Create a free ScreenshotNeo account to use the 1,000 monthly screenshots without a card.

Frequently Asked Questions

Why does the error mention an object when the selector is a string?

The selector is only the input. The failing value is the result of the lookup: when no node matches, that result is null, and the next property or method access triggers the TypeError.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Should I treat document.readyState === 'complete' as proof that my target exists?

No. A complete document can still lack an element that a script will create later, and an element can exist before the application has populated it. Use the specific DOM condition your operation requires.

Can one selector search across the main page and every iframe?

No. Each iframe has its own document. Locate the frame, switch into it, perform the query there, and switch back when finished.

What is the safest value to return from page.evaluate?

Return serialized primitives or plain objects containing the exact fields the outer script needs. Extract text, dimensions, attributes, or booleans inside the page context instead of returning DOM objects.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.