How to Pass Arguments to page.evaluate() in PhantomJS
Learn the correct page.evaluate(function, ...args) syntax in PhantomJS, serialization rules, scope boundaries, debugging, and safer screenshot options.

Direct answer: put the function first and pass each value after it, in the same order as the function parameters. PhantomJS documents the signature as page.evaluate(function, arg1, arg2, ...). The values crossing the boundary must be JSON-serializable.
var text = page.evaluate(function(selector) {
var element = document.querySelector(selector);
return element ? element.innerText : null;
}, 'h1');
Here, 'h1' becomes the evaluated function’s selector argument. This argument-passing form was added in PhantomJS 1.6, according to the official WebPage.evaluate reference.
What page.evaluate() does
page.evaluate() executes JavaScript inside the loaded webpage. Your PhantomJS script runs in an outer context; the callback runs in the page context, with access to the document, window, DOM nodes and page JavaScript. The callback does not automatically inherit variables, functions or closures from the outer script. Pass every value it needs explicitly.
The boundary is useful because it lets a PhantomJS program inspect or modify a page without injecting a separate script file. It also explains the most common error: referencing an outer variable by name inside the callback and receiving ReferenceError: Can't find variable.
PhantomJS is legacy software. The API behavior described here is documented for PhantomJS; when maintaining an existing system, check the installed version and its compatibility with your page.
Minimal working example
The following complete script opens a page, checks the load status, passes a selector into evaluate(), returns plain text, and exits cleanly.

var page = require('webpage').create();
page.open('https://example.com', function(status) {
if (status !== 'success') {
console.log('Unable to load page');
phantom.exit(1);
return;
}
var heading = page.evaluate(function(selector) {
var element = document.querySelector(selector);
return element ? element.textContent : null;
}, 'h1');
console.log(heading === null ? 'No matching element' : heading);
phantom.exit();
});
Always inspect the result of page.open() before reading page content. A successful callback means PhantomJS loaded the URL according to its page-loading rules; it does not guarantee that your selector exists or that every asynchronous application request has finished.
Passing one or more arguments
One primitive value
var title = page.evaluate(function(selector) {
var node = document.querySelector(selector);
return node ? node.textContent.trim() : null;
}, 'title');
Strings, numbers, booleans and null are the simplest values to pass. The argument order is positional, so the first value after the callback is assigned to the first callback parameter.
Several values
var result = page.evaluate(function(selector, minimumLength, includeHidden) {
var node = document.querySelector(selector);
if (!node) {
return { found: false, text: null };
}
var text = includeHidden ? node.textContent : node.innerText;
text = text.trim();
return {
found: text.length >= minimumLength,
text: text
};
}, '.description', 20, false);
The callback receives '.description', then 20, then false. Keep the callback signature and trailing argument list visually aligned; it prevents accidental reordering during maintenance.
Objects and arrays
Use a plain object when several related settings belong together.
var data = page.evaluate(function(options) {
var nodes = document.querySelectorAll(options.selector);
var values = [];
for (var i = 0; i < nodes.length; i += 1) {
if (options.attribute) {
values.push(nodes[i].getAttribute(options.attribute));
} else {
values.push(nodes[i].textContent.trim());
}
}
return values;
}, {
selector: '.product a',
attribute: 'href'
});
PhantomJS uses JSON serialization as the rule of thumb for arguments and return values. Prefer data that could be represented by JSON.stringify(): nested objects, arrays, strings, numbers, booleans and null. Avoid circular references, undefined values and host objects.
Scope: why outer variables are unavailable
This code is a common mistake:
var selector = 'h1';
var text = page.evaluate(function() {
return document.querySelector(selector).textContent;
});
selector belongs to the PhantomJS script, while the callback runs in the webpage. It was never supplied to the page context. Pass it explicitly:
var selector = 'h1';
var text = page.evaluate(function(s) {
var node = document.querySelector(s);
return node ? node.textContent : null;
}, selector);
This is a context boundary, not a JavaScript closure. The PhantomJS quick start also describes the limitations on values returned from the page.
What can cross the boundary
| Value | Use | Guidance |
|---|---|---|
| String, number, boolean | Selectors, flags, limits | Safe and straightforward. |
| Null | Explicitly empty option | Useful when a value is optional. |
| Plain object or array | Configuration and result sets | Keep members JSON-serializable. |
| Function | Callback or helper | Unsupported across the boundary. |
| DOM node | Selected element | Do not pass it out or in; extract text or attributes. |
| Closure | Outer lexical state | Unavailable; pass the needed data instead. |
The API explicitly warns that closures, functions and DOM nodes do not work as transferred values. Return a compact data object rather than a live element.
Returning useful data
var links = page.evaluate(function(selector) {
var nodes = document.querySelectorAll(selector);
var output = [];
for (var i = 0; i < nodes.length; i += 1) {
output.push({
text: nodes[i].textContent.trim(),
href: nodes[i].href
});
}
return output;
}, 'a.navigation-link');
Return strings, numbers, booleans, arrays and plain objects. Do not return an element, a function or an object containing one. If a value may be missing, return null or an explicit property such as found: false.
Waiting for dynamic content
evaluate() runs when you call it. It does not wait for a framework render, an AJAX request or a timer. For a page that fills its heading later, poll for a condition from the outer script.
var system = require('system');
var page = require('webpage').create();
var attempts = 0;
page.open('https://example.com/app', function(status) {
if (status !== 'success') {
console.log('Unable to load page');
phantom.exit(1);
return;
}
function readHeading() {
var heading = page.evaluate(function() {
var node = document.querySelector('[data-ready="true"] h1');
return node ? node.textContent.trim() : null;
});
if (heading !== null || attempts >= 20) {
console.log(heading === null ? 'Timed out waiting for heading' : heading);
phantom.exit(heading === null ? 1 : 0);
return;
}
attempts += 1;
window.setTimeout(readHeading, 250);
}
readHeading();
});
For complex applications, wait on a page-specific marker instead of sleeping for an arbitrary long period. Keep a maximum attempt count so a broken page cannot hold the process forever.
Forwarding console messages
Messages logged inside the page context are not printed in the PhantomJS terminal automatically. Register page.onConsoleMessage when debugging:
var page = require('webpage').create();
page.onConsoleMessage = function(message, lineNumber, sourceId) {
console.log('[page] ' + sourceId + ':' + lineNumber + ' ' + message);
};
page.open('https://example.com', function(status) {
if (status !== 'success') {
phantom.exit(1);
return;
}
page.evaluate(function(selector) {
console.log('Looking for ' + selector);
return !!document.querySelector(selector);
}, 'h1');
phantom.exit();
});
When possible, return the value needed by the outer script instead of relying on console output. Console forwarding is best for diagnostics.
evaluateJavaScript() is different
page.evaluateJavaScript(str) accepts a string containing a function declaration and invokes it. The evaluateJavaScript reference demonstrates setting and reading page globals in separate calls, but it does not document the same trailing-argument form. For ordinary argument passing, use page.evaluate(function, ...args).
String evaluation also makes quoting and escaping harder. If a value is user-controlled, avoid concatenating it into JavaScript source. Pass it as a serialized argument to evaluate() instead.
Practical patterns
Read an attribute safely
var canonical = page.evaluate(function(selector) {
var node = document.querySelector(selector);
return node ? node.getAttribute('href') : null;
}, 'link[rel="canonical"]');
Set a form value
var changed = page.evaluate(function(selector, value) {
var input = document.querySelector(selector);
if (!input) return false;
input.value = value;
return true;
}, '#email', 'developer@example.com');
Use a selector list
var counts = page.evaluate(function(selectors) {
var result = {};
for (var i = 0; i < selectors.length; i += 1) {
result[selectors[i]] = document.querySelectorAll(selectors[i]).length;
}
return result;
}, ['h1', 'img', 'a']);
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
Can't find variable selector |
The callback references an outer variable. | Add a callback parameter and pass the value after the function. |
| Argument is undefined | Arguments are in the wrong order or were omitted. | Compare the callback parameter order with the trailing values. |
| Element is null | The selector does not match or content is not rendered yet. | Check the selector, verify status, and poll for a readiness marker. |
| Returned object is empty or incomplete | It contains unsupported DOM nodes, functions or unserializable values. | Map the result to strings, numbers, booleans and plain objects. |
| Page logs are missing | Page-context console output is not forwarded by default. | Set page.onConsoleMessage. |
| Script hangs | A wait loop has no deadline or the page never reaches the condition. | Use a bounded retry count and exit with a failure status. |
| Works locally but not in production | Different PhantomJS versions, user agents, TLS support or page timing. | Record the version, URL, status and timing; reproduce with the same runtime. |
Performance, reliability and cost
Each call crosses the browser/page boundary and serializes its arguments and result. Pass one configuration object and return one compact result when practical instead of making dozens of tiny calls. Avoid returning full HTML or large arrays if the caller needs only a few fields.
DOM queries can be expensive on very large pages. Narrow selectors, query once, and process the resulting nodes in one evaluation. Dynamic pages need bounded waits; an unlimited polling loop harms reliability more than a short, explicit timeout.
For repeatable automation, log the URL, PhantomJS version, open status, selector, retry count and a concise error. Treat a missing element as a handled outcome when it is valid for the page, and as a failure when it violates your contract.
PhantomJS itself has no per-call ScreenshotNeo-style billing model; your cost is the infrastructure and runtime needed to operate it. If you only need an image or PDF, maintaining a headless browser, page waits and cleanup code may be unnecessary.
Or skip the browser setup
If the goal is a clean website screenshot rather than DOM automation, ScreenshotNeo provides a one-request screenshot API and MCP server. Cookie and consent banners are accepted and removed before capture, along with 60+ known consent platforms, newsletter popups and chat widgets. Bot checks, blank pages, timeouts, failed loads and cache hits are not billed, and response headers identify the page verdict and billing result.

See the ScreenshotNeo API documentation for all capture options. The basic calls are:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
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)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
You can select PNG, JPEG or WebP output, capture full pages with lazy images, target one CSS element, set a device or viewport, use dark mode and retina scale, inject CSS or JavaScript, click before capture, wait for a selector, delay or network idle, block ads and resource types, set headers, cookies, user agent, timezone or geolocation, resize images, choose a cache TTL, create signed image links, run asynchronous jobs with signed webhooks, submit up to 100 URLs in one bulk call, and use PDF options such as paper size, margins, landscape and page ranges. An MCP server exposes take_screenshot, get_page_info and capture_pdf to Claude, Cursor and other MCP clients.
There are 1,000 free screenshots each month with no card. Paid plans start at $5 for 3,000 screenshots; every feature is available on every plan. Create a free ScreenshotNeo account.
FAQ
Which PhantomJS version supports evaluate arguments?
The official API states that JSON-serializable arguments are supported as of PhantomJS 1.6. Confirm the version installed in the environment you maintain.
Can I pass a DOM element into evaluate()?
No. DOM nodes are page-context objects. Pass a selector or extracted primitive data, then query the element inside the callback.
Can the callback access variables declared outside it?
No. Pass those values as trailing arguments.
Why does my returned value become null?
Inspect the return shape for unsupported values such as functions, closures or DOM nodes. Convert it to JSON-serializable data before returning.
Should new projects use PhantomJS?
PhantomJS is legacy software. For an existing script, use the documented API and pin the runtime. For screenshot delivery without browser maintenance, evaluate ScreenshotNeo or another current service against your requirements.


