How to Fix Pyppeteer Evaluation Failed: Unexpected Token Return
Fix Pyppeteer’s “Unexpected token return” error by passing a function to evaluation, then troubleshoot wrappers, versions, and browser failures.
Direct fix: the return statement is being parsed at the top level of the JavaScript supplied to the renderer. Put it inside a complete function expression, such as () => { ... }, when calling requests-html’s render(script=...).
from requests_html import HTMLSession
session = HTMLSession()
response = session.get("https://example.com/chart")
script = """() => {
return Highcharts.charts[0].series[0].data.map(d => d.y);
}"""
result = response.html.render(script=script, reload=False)
print(result)
A bare string beginning with return is not valid as a top-level JavaScript expression. The browser raises SyntaxError: Unexpected token return before your chart code runs.
1. What the error means
JavaScript only permits return inside a function body. This fails:
return Highcharts.charts[0].series[0].data.map(d => d.y);
This is valid because the return statement belongs to an arrow function:
() => {
return Highcharts.charts[0].series[0].data.map(d => d.y);
}
The important detail is the API receiving the string. The reported failure uses response.html.render(script=...) from requests-html. That wrapper’s demonstrated working form is a complete function expression. Direct Pyppeteer’s Page.evaluate accepts a function or an expression and exposes a force_expr option, so do not assume that every wrapper parses strings identically. See the Pyppeteer 0.0.25 API reference and the current Puppeteer Page.evaluate documentation.
2. Working requests-html example
Return chart data
from requests_html import HTMLSession
session = HTMLSession()
resp = session.get("https://example.com/chart")
script = """() => {
const chart = Highcharts.charts[0];
if (!chart) {
return {error: "No Highcharts chart found"};
}
return chart.series[0].data.map(point => point.y);
}"""
value = resp.html.render(script=script, reload=False)
print(value)
Use a block body for multiple operations
script = """() => {
const chart = Highcharts.charts[0];
const values = chart.series[0].data.map(point => point.y);
document.body.dataset.extracted = JSON.stringify(values);
return values;
}"""
values = resp.html.render(script=script, reload=False)
print(values)
Use an expression when you do not need return
An arrow function with an expression body returns that expression implicitly:
script = "() => Highcharts.charts[0].series[0].data.map(d => d.y)"
values = resp.html.render(script=script, reload=False)
This form avoids an explicit return, but the braced form is easier to extend and debug.
3. Direct Pyppeteer: function versus expression
When you call Pyppeteer directly, keep the evaluation boundary explicit:
import asyncio
from pyppeteer import launch
async def main():
browser = await launch()
page = await browser.newPage()
await page.goto("https://example.com/chart", {"waitUntil": "networkidle2"})
values = await page.evaluate("""() => {
return Highcharts.charts[0].series[0].data.map(d => d.y);
}""")
print(values)
await browser.close()
asyncio.get_event_loop().run_until_complete(main())
Pyppeteer documents evaluation as accepting a JavaScript function or expression. If you intentionally provide an expression string, consult the installed version’s behavior and its force_expr setting rather than copying rules from requests-html. A function is generally the clearest input because the return context is unambiguous.
4. A reliable debugging sequence
- Identify the caller. Record whether the failing line is
html.render(script=...),page.evaluate(...), or another wrapper. - Print the exact JavaScript string. Hidden concatenation, indentation, or an accidental prefix can change what the browser parses.
- Wrap the body. For requests-html, use
() => { ... }and place everyreturninside the braces. - Reduce the script. First evaluate
() => 1, then access the chart, then map its data. This separates parser errors from page-state errors. - Check page readiness. A valid function can still fail if the chart library has not loaded or the chart has not been created.
- Capture versions. Save Python, requests-html, Pyppeteer, Chromium, and operating-system versions for reproducibility.
- Read the full traceback. A syntax error occurs before page code executes; later errors such as
TypeErroror missing selectors have different causes.
5. Common errors and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
Unexpected token return |
Top-level return in the supplied string |
Pass () => { return ... } (requests-html) or a valid function/expression for the API you call. |
Highcharts is not defined |
The library has not loaded, or the URL is different | Wait for the library or a chart selector, verify the page URL, and inspect console/network errors. |
Cannot read properties of undefined |
Highcharts.charts[0] does not exist yet |
Wait for chart creation and check for a missing chart before reading series. |
Evaluation returns None |
The callback has no returned value, or the wrapper does not expose it as expected | Add an explicit return and verify the wrapper’s result semantics. |
| Works in one wrapper but not another | Different string parsing or expression handling | Use that library’s documented function/expression form and test a minimal callback. |
| Browser launch or protocol errors | Chromium and Pyppeteer version mismatch, sandbox restrictions, or a damaged browser download | Use the bundled Chromium where possible, record versions, reinstall the browser, and review launch flags for the deployment environment. |
| Timeout while rendering | Slow resources, never-ending network activity, or a page that requires interaction | Set an appropriate navigation timeout, wait for a specific selector, block unnecessary resources, and close the browser in a finally block. |
6. Waiting for dynamic charts
Syntax correctness does not guarantee that the data exists when evaluation starts. Prefer a page-state wait over an arbitrary sleep:
await page.waitForFunction("""() => {
return window.Highcharts &&
Highcharts.charts &&
Highcharts.charts[0] &&
Highcharts.charts[0].series.length > 0;
}""", {"timeout": 30000})
values = await page.evaluate("""() =>
Highcharts.charts[0].series[0].data.map(d => d.y)
""")
With requests-html, keep the function wrapper and choose reload=False when the existing loaded page should be evaluated without another navigation. If the site renders data after an API call, wait for the resulting DOM or JavaScript state rather than assuming networkidle means the chart is ready.
7. Resource handling and reliability
- Always close the browser in a
finallyblock so failed evaluations do not leak Chromium processes. - Keep the smallest failing script and page URL; this makes parser and timing problems separable.
- Use the Chromium version bundled with your Pyppeteer release when possible. The Pyppeteer documentation does not guarantee compatibility with arbitrary Chromium versions.
- Pin package versions in deployment and log them with each failure.
- For repeated captures, reuse a browser process but create isolated pages or contexts to avoid state leaking between URLs.
- Return structured error data from your callback when a chart is absent instead of dereferencing
undefined.
8. Performance and cost considerations
Launching Chromium dominates short scripts, so reusing a browser can reduce latency. Waiting for a precise selector or JavaScript condition is usually faster and more reliable than a long fixed sleep. Blocking images, ads, analytics, or unrelated resource types can help when your use case only needs chart data, but verify that the chart itself does not depend on a blocked request.
Self-hosted Pyppeteer cost comes from your compute, browser memory, bandwidth, and engineering time. There is no per-evaluation API charge, but failed browser launches and page timeouts still consume infrastructure resources. Measure concurrency limits in your own environment before increasing parallel pages.
9. Or skip the browser setup
If your goal is a clean image or PDF rather than executing a chart extraction callback, ScreenshotNeo provides a single screenshot API request. Its browser handles consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be turned off. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and the response identifies the result with X-Page-Verdict and X-Billed headers. An MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.
See the ScreenshotNeo API documentation for all options.
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)
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}`);
Features include full-page and element capture, dark mode, device presets, custom viewports, retina scale, PDF options, custom CSS and JavaScript, selector waits, request blocking, headers, cookies, user agents, geolocation, caching, signed links, asynchronous jobs, bulk capture, and a usage API. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.
10. FAQ
Can I put return at the top level if I use an async callback?
No. async () => { return value; } is valid because the return remains inside the function. A top-level return is still invalid.
Should I use a string or a function with Pyppeteer?
Use a function when possible. It makes the return context explicit and is easier to debug. Check your installed Pyppeteer version for expression and force_expr details.
Does this error prove the chart library is broken?
No. The reported error is a JavaScript input syntax failure. Once fixed, you may still need to handle loading, selectors, authentication, or chart state separately.
What information should I include in a bug report?
Include the exact API call, the complete script string, package and Chromium versions, target URL, operating system, and full traceback.
Summary: for the reported requests-html case, replace a top-level return with the complete arrow function () => { ... }. Then debug page readiness and browser compatibility as separate issues.


