Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.
page.evaluate() returns whatever its callback returns. If a callback path reaches its end without a return, Puppeteer resolves the call to JavaScript undefined. For an AtCoder scraper, first check every return path; then verify that the callback uses data available in the browser page, returns serializable values, and runs only after the contest data you need is present.
What undefined means in this case
Puppeteer’s Page.evaluate() API describes the method as evaluating a function in the page’s context and returning its result. The value you receive in Node.js is therefore the callback’s result—not an automatic extraction of the page or of a variable you happened to inspect in the browser.
In JavaScript, a function that finishes without returning a value returns undefined. That can happen accidentally when the callback has no return, or when one branch returns a value but another falls through. A missing selector or data that has not loaded can lead to that second kind of bug if the code handles the case without returning anything.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Without the callback, exact contest URL, and page state, it is not possible to say which cause applies to a particular scraper. The checks below isolate the common causes in a useful order.
Check that every callback path returns a value
Put the return inside the function passed to page.evaluate(). Returning from the surrounding Node.js function does not return a value from the browser-side callback.
const result = await page.evaluate(() => {
const heading = document.querySelector("h1");
return heading?.textContent?.trim() ?? null;
});
console.log(result);
This example returns the heading text when an h1 exists and null when it does not. That makes “the page has no matching heading” distinguishable from an accidentally omitted return. The selector is only an example: confirm the actual structure of the contest page you are scraping rather than assuming one selector works for every AtCoder page.
Look for a branch that falls through
Review every conditional branch and every early exit in the callback. A callback like this returns a string only if the element exists; otherwise it reaches the end and yields undefined:
const title = await page.evaluate(() => {
const heading = document.querySelector("h1");
if (heading) {
return heading.textContent.trim();
}
// No return on this path: result is undefined.
});
Choose an explicit missing-data policy: return null, return a descriptive string, or throw an error that Node.js can catch. Avoid silently allowing a missing element to look like a successful extraction.
Rank #2
Remember that evaluation runs in the page, not in Node.js
Puppeteer serializes the supplied function and executes it in the target page. It does not give that function access to Node.js variables or helper functions from the surrounding lexical scope. If the callback references a Node-only variable, it will not behave as though it were running alongside your Node code.
Pass values needed by the browser callback as arguments, and keep the callback’s logic self-contained:
const selector = "h1";
const text = await page.evaluate((selector) => {
const element = document.querySelector(selector);
return element?.textContent?.trim() ?? null;
}, selector);
For more involved extraction, either pass the required serializable inputs or define the necessary helper logic inside the callback. Keep browser-side and Node-side responsibilities clear: the callback reads page data and returns it; the surrounding Node.js code handles the returned value.
Return data that evaluation can serialize
Ordinary evaluation is designed to return a value that Puppeteer can serialize back to Node.js. Returning a DOM node does not give Node.js a usable live DOM object; Puppeteer’s guide shows that returning document.body this way produces {}. Extract the fields you need in the page and return plain data instead:
const contestInfo = await page.evaluate(() => {
const heading = document.querySelector("h1");
return {
title: heading?.textContent?.trim() ?? null,
url: location.href
};
});
If you genuinely need a reference to an object in the page context, use Puppeteer’s evaluateHandle() rather than expecting ordinary evaluation to return a live DOM object. For contest scraping, returning text, attributes, arrays, or plain objects is usually simpler to inspect and consume.
Wait for the contest data you actually need
A page can be open while the element or data your callback expects is still absent. Choose a wait condition tied to the extraction target—for example, a selector that appears when the relevant contest content is rendered—then evaluate. Waiting for network activity to become idle is not proof that a particular selector exists or contains the expected data.
If clicking a link triggers navigation, Puppeteer documents waiting for navigation alongside the click using Promise.all:
await Promise.all([
page.waitForNavigation(),
page.click("a.contest-link")
]);
await page.waitForSelector("h1");
const title = await page.evaluate(() => {
const heading = document.querySelector("h1");
return heading?.textContent?.trim() ?? null;
});
Replace the sample selector with one verified for the target page and the information being extracted. waitForNavigation() can resolve with null for hash changes or History API navigation, so a successful wait does not necessarily mean a traditional document navigation occurred. Similarly, waitForNetworkIdle() indicates a network-idle condition, not that your desired contest data was found. Combine waits only when they correspond to conditions your scraper needs.
Rank #4
Rendered page or contest JSON route?
A community-maintained AtCoder client documents a standings route in JSON form and a contest tasks page. For a known contest ID, those references are:
https://atcoder.jp/contests/{contest_id}/standings/jsonhttps://atcoder.jp/contests/{contest_id}/tasks
The JSON route may be a simpler source for standings data than extracting rendered DOM, but community documentation is not an official guarantee that the route works for every contest or under every access condition. Check the response for the exact contest and consider AtCoder’s current rules before depending on it. The tasks route is a page URL, not proof that all the information your program needs is available in a particular JSON response.
If using the AtCoder Problems project’s API, note that the project describes it as unofficial and warns that APIs may be deprecated or replaced. Its documentation asks users to leave more than one second between accesses. Check its current documentation and avoid frequent requests; do not treat an unofficial route as a permanent contract.
Recommended Free Tools
A practical debugging sequence
- Log the exact value returned. Immediately after
await page.evaluate(...), log the result and, if useful, whether it isnullorundefined. - Make the callback return a constant. Temporarily return a known string such as
"callback ran". If that appears, evaluation is reaching Node.js and the extraction logic is the next place to inspect. - Return a missing-element marker. Use optional chaining with
?? nullso a missing selector has a visible result instead of falling through. - Check the selector on the actual page. Confirm it matches the desired element on the exact contest page. Do not assume a universal AtCoder DOM selector.
- Check execution context. Pass needed Node.js values as callback arguments; do not rely on closure variables or Node-only helper functions being available in the page.
- Check the result’s shape. Return text, attributes, or plain objects rather than a DOM node when using ordinary evaluation.
- Wait for the extraction condition. Wait for the target selector or other meaningful page condition, not only for network idleness.
- Inspect route behavior separately. If testing a documented JSON route, examine the response for that contest and account for its community-maintained, unofficial status.
Common symptoms and fixes
| Symptom | Likely explanation | What to do |
|---|---|---|
The result is exactly undefined |
The callback has no explicit return on the executed path. | Return the extracted value on every branch, including missing-element branches. |
| The callback refers to a Node.js variable or helper | Page evaluation runs in the browser context and cannot reach the Node.js closure. | Pass serializable values as arguments or define the logic inside the callback. |
| A returned DOM element becomes an empty-looking object | Ordinary evaluation serializes the result; it does not return a usable live DOM node. | Extract the element’s text or attributes, or use evaluateHandle() if a page-object reference is required. |
| The element is absent immediately after navigation | The target content may not yet be present, or the selector may not match that page. | Check the selector and wait for the specific content condition before evaluating. |
| Network idle occurs but extraction still fails | Network idleness alone does not establish that the desired element or data exists. | Wait for the target selector or a predicate tied to the content being extracted. |
| A documented route works for one contest but not another | The community reference does not guarantee coverage or access conditions for every contest. | Inspect the exact response and verify current access conditions; keep a rendered-page approach where appropriate. |
Or skip the browser setup
If your goal is a clean visual capture of a contest page rather than structured contest data, ScreenshotNeo is a website screenshot API and MCP server. It is not a replacement for extracting standings fields into JSON. For screenshots, one GET request can return a PNG, JPEG, WebP, or PDF. This cURL example saves a WebP capture of the AtCoder tasks page; substitute the exact contest ID you need.
Best Value
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://atcoder.jp/contests/CONTEST_ID/tasks -o shot.webp
See the ScreenshotNeo documentation for API parameters. It accepts cookie or consent banners 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 cost nothing, with the result identified in response headers. Its MCP server offers take_screenshot, get_page_info, and capture_pdf for AI agents and MCP clients.
The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Sign up for ScreenshotNeo’s free plan.
Frequently Asked Questions
Does page.evaluate() return null when a selector is missing?
Not automatically. The callback must explicitly return null for that case; otherwise, if it falls through, the result is undefined.
Can I use an AtCoder JSON route for every contest?
The community-maintained route reference does not establish universal coverage. Check the exact contest’s response and current access conditions before relying on it.
Quick Recap
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.

