To select the second data row, scope a CSS selector to the intended table body and use tbody tr:nth-of-type(2). For text extraction, run it with page.$eval(); when a second row might not exist, use page.$$eval() and return rows[1] ?? null instead of allowing a missing match to throw.
The selector that usually means “second data row”
Most tables put column headings in <thead> and data in <tbody>. In that markup, the second data row is:
table tbody tr:nth-of-type(2)
A direct extraction with Puppeteer is:
const secondRowText = await page.$eval(
'table tbody tr:nth-of-type(2)',
row => row.textContent,
);
console.log(secondRowText);
Puppeteer passes the first element matching the selector to the callback. If no second row exists, $eval throws. That behavior is useful when the row is required, but a list-based query is safer when an empty or short table is valid.
const secondRowText = await page.$$eval(
'table tbody tr',
rows => rows[1]?.textContent ?? null,
);
if (secondRowText === null) {
console.log('The table has fewer than two data rows.');
}
The index is zero-based, so rows[1] is the second match. Returning null makes the absence explicit instead of producing an “element not found” exception.
The Tool Desk
Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →#1 Best Overall
- Read Before You Buy — No Video Output: These adapters support charging and USB 2.0 data transfer, but cannot transmit video signals. Except for standard USB webcams (which use USB data only), they are not compatible with HDMI/DisplayPort cables, video-capable USB-C hubs, or docking stations with video output.
- Convert USB-A Ports to USB-C: Designed to connect USB-C earphones, cables, flash drives, card readers, and other USB-C accessories to standard USB-A ports. Plug-and-play with no drivers or software required.
- Aluminum Alloy Housing: Built with a sturdy aluminum alloy shell that aids in heat dissipation and protects against daily wear and scratches. Designed to maintain a stable and secure connection.
- Compact & Travel-Friendly: The ultra-compact design allows the adapter to stay plugged into your device without blocking adjacent ports or adding bulk, reducing wear and tear on your original USB ports.
- 12-Month Warranty: Backed by a 12-month manufacturer warranty for peace of mind. Designed to meet strict quality control standards for reliable everyday performance.
Choose the right CSS positional selector
nth-of-type() and nth-child() are not interchangeable. The correct choice depends on the siblings in the actual DOM.
| Selector | What it counts | Typical use |
|---|---|---|
tbody tr:nth-of-type(2) |
The second tr among tr siblings in that tbody |
Second data row when the body contains rows |
tbody tr:nth-child(2) |
The element that is the second child of the tbody, regardless of type |
Only when you know every preceding child and its type |
table tr:nth-of-type(2) |
The second tr in the selected table context |
When header rows should be included in the count |
#results tbody tr:nth-of-type(2) |
The second row inside the table identified by results |
Pages containing multiple tables |
Why tbody matters
If the requirement is “second data row,” scope to tbody. A selector such as table tr:nth-of-type(2) can count a row in the header section, depending on the table structure. Inspect whether rows are under thead, tbody, tfoot, or a nested table before choosing the scope.
Why nth-of-type(2) can be safer than nth-child(2)
nth-child(2) counts element position among all children. An inserted element, template marker, or other element node can change that position. nth-of-type(2) counts only siblings of the same element type, which matches the usual meaning of “second tr.” It still counts only rows in the parent selected by your scope.
Scope the selector when a page has several tables
Positional selection is meaningful only relative to the elements matched by the selector. A broad query may select the second row of an unrelated table. Prefer a stable identifier or container:
Recommended Free Tools
Rank #2
- 5-in-1 USB-C Hub: Experience comprehensive connectivity featuring a Power Delivery input, two USB-A 2.0 ports, a USB-A 3.0 port, and an HDMI port. (Note: The USB-C power delivery input port is only for connecting an external wall charger to power your laptop and cannot power peripheral devices.)
- 90W Pass-Through Charging: Achieve optimal charging with 90W pass-through power to your laptop, supported by a total input of 100W, with the hub reserving 10W for operational efficiency. (Note: Wall charger not included.)
- Quick Data Transfers: Accelerate your productivity with rapid data transfers using a high-speed 5Gbps USB 3.0 port and two 480Mbps USB 2.0 ports.
- 4K HDMI Display: Enhance your visual experience with a hub capable of delivering 4K resolution at 30Hz in both mirror and extend modes. Please note that this hub is compatible with MacBook (macOS 12 and newer), Windows 10 and 11, ChromeOS, and laptops equipped with DP Alt Mode and Power Delivery. Note: This device is not compatible with Linux.
- What You Get: Anker USB-C Hub (5-in-1, 4K HDMI), welcome guide, 18-month warranty, and our friendly customer service.
const row = await page.$eval(
'#results tbody tr:nth-of-type(2)',
el => ({
text: el.textContent.trim(),
cells: [...el.querySelectorAll('td')].map(cell => cell.textContent.trim()),
}),
);
If there is no ID, use a stable class, a data attribute, or an ancestor tied to the table’s purpose. Avoid relying on generated class names that change between builds. If the table is nested, include the full stable path so a nested table cannot satisfy the selector accidentally.
Read the row with $eval or $$eval
Use $eval when the row must exist
$eval runs a callback against the first matching element and fails immediately when there is no match. That is appropriate for an assertion or a page contract where a missing second row is an error.
const secondRow = await page.$eval(
'#results tbody tr:nth-of-type(2)',
row => ({
text: row.textContent.trim(),
cells: Array.from(row.querySelectorAll('td'), cell => cell.textContent.trim()),
link: row.querySelector('a')?.href ?? null,
}),
);
Use $$eval when absence is expected
$$eval supplies the complete match array. It does not throw merely because the array is empty, so indexing with optional chaining gives a deliberate result:
const secondRow = await page.$$eval(
'#results tbody tr',
rows => {
const row = rows[1];
return row
? {
text: row.textContent.trim(),
cells: Array.from(row.querySelectorAll('td'), cell => cell.textContent.trim()),
}
: null;
},
);
Use the same pattern to collect a particular cell, attribute, or normalized value. The callback executes in the page context, so browser DOM APIs such as querySelectorAll are available inside it.
Rank #3
- Sleek 7-in-1 USB-C Hub: Features an HDMI port, two USB-A 3.0 ports, and a USB-C data port, each providing 5Gbps transfer speeds. It also includes a USB-C PD input port for charging up to 100W and dual SD and TF card slots, all in a compact design.
- Flawless 4K@60Hz Video with HDMI: Delivers exceptional clarity and smoothness with its 4K@60Hz HDMI port, making it ideal for high-definition presentations and entertainment. (Note: Only the HDMI port supports video projection; the USB-C port is for data transfer only.)
- Double Up on Efficiency: The two USB-A 3.0 ports and a USB-C port support a fast 5Gbps data rate, significantly boosting your transfer speeds and improving productivity.
- Fast and Reliable 85W Charging: Offers high-capacity, speedy charging for laptops up to 85W, so you spend less time tethered to an outlet and more time being productive.
- What You Get: Anker USB-C Hub (7-in-1), welcome guide, 18-month warranty, and our friendly customer service.
Interact with a control inside the second row
Extraction and interaction have different waiting needs. Puppeteer’s documentation describes Locators as the recommended way to select an element and interact with it. A locator can target the row and then the button within it:
const secondRow = page.locator('#results tbody tr:nth-of-type(2)');
await secondRow.locator('button[data-action="details"]').click();
This keeps the row scope attached to the control and lets Locator action conditions handle the element’s readiness. If your installed Puppeteer version does not provide the Locator API, wait for the selector explicitly and use a selector-based click:
const rowSelector = '#results tbody tr:nth-of-type(2)';
const buttonSelector = `${rowSelector} button[data-action="details"]`;
await page.waitForSelector(buttonSelector);
await page.click(buttonSelector);
Use a more specific control selector when a row contains several buttons. Do not click a page-wide button selector and assume it belongs to the second row.
Wait for rows that are rendered dynamically
Client-side rendering can create the table after the initial navigation. Querying immediately may produce an empty result even though the row will appear later. Wait for the scoped selector or for a page condition that represents completion:
Rank #4
- Dual Converters, Infinite Potential:Includes 2× USB C male to USB A female adapters and 2× USB A male to USB C female adapters. Perfect for a wide range of uses—tablets with Bluetooth keyboards, expand USB ports on macbook, and more. Two different converters for all your daily needs
- Next-Level 10Gbps & 3A Charging: No more slow 480Mbps, this usb to usb c adapter has a transfer speed of up to 10Gbps, allowing you to do more transferring in less time. This usb adapter fits both USB A and USB C charger, supporting up to 3A fast charging
- Upgraded Exquisite Craftsmanship: With an aluminum alloy housing and metal connector, the usbc to usb adapter is extremely durable and sturdy. Rigorously tested to withstand more than 10,000 times of plugging and unplugging, ensuring long-lasting performance
- Broad Compatible: The usb c to usb adapter widely supports all USB C/ USB A devices like laptops, tablets, cellphones, car chargers, and phone chargers. Such as compatible with MacBook Pro/Air 2023/2022, Thunderbolt 4/3 Devices,Apple MagSafe Watch 9/8/7/SE/Ultra, iPad Pro 2022/2021, Samsung Galaxy S23/S20/S10, and iPhone 17/16/15 Pro. Plug and play
- Please Note: To reach 10Gbps speed, keep the cable under 3.3 ft. For USB A Male to USB C adapters, try flipping the USB C connector. USB C Male to USB A adapters support bidirectional 10Gbps transfer within 3.3 ft
await page.goto('https://example.com/results', { waitUntil: 'networkidle2' });
await page.waitForSelector('#results tbody tr:nth-of-type(2)');
const text = await page.$eval(
'#results tbody tr:nth-of-type(2)',
row => row.textContent.trim(),
);
networkidle2 is not a guarantee that a framework has finished updating the DOM. A page-specific selector is usually the stronger signal. If a table can legitimately contain one row, wait for the first row and then inspect the collection without treating the missing second row as a timeout:
await page.waitForSelector('#results tbody tr');
const second = await page.$$eval(
'#results tbody tr',
rows => rows[1]?.textContent.trim() ?? null,
);
For paginated or virtualized tables, the second visible row may not represent the second row in the full dataset. Load the needed page or scroll the component according to its application behavior before selecting by position.
A complete Puppeteer example
The following script navigates to a page, waits for the table, extracts the second data row, and reports a missing row without crashing. Replace the URL and table ID with values from your page.
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch();
try {
const page = await browser.newPage();
await page.goto('https://example.com/results', {
waitUntil: 'networkidle2',
timeout: 30_000,
});
await page.waitForSelector('#results tbody tr');
const secondRow = await page.$$eval(
'#results tbody tr',
rows => {
const row = rows[1];
if (!row) return null;
return {
text: row.textContent.trim(),
cells: Array.from(row.querySelectorAll('td'), cell => cell.textContent.trim()),
};
},
);
if (secondRow === null) {
console.log('No second data row was rendered.');
} else {
console.log(secondRow);
}
} finally {
await browser.close();
}
})();
If the row is mandatory, change the final query to $eval('#results tbody tr:nth-of-type(2)', ...) so a missing row fails the run and can be handled by your test runner.
PC Slower Than It Used to Be?
A free scan shows the junk files, broken settings and background clutter dragging Windows down - then fixes them in one click.Free scan · Windows 10 & 11Outdated Drivers Are Slowing You Down
One free scan finds every outdated or missing driver and matches the right update for your exact hardware.Free scan · exact hardware matchBest Value
- 5-in-1 Connectivity: Equipped with a 4K HDMI port, a 5 Gbps USB-C data port, two 5 Gbps USB-A ports, and a USB C 100W PD-IN port. Note: The USB C 100W PD-IN port supports only charging and does not support data transfer devices such as headphones or speakers.
- Powerful Pass-Through Charging: Supports up to 85W pass-through charging so you can power up your laptop while you use the hub. Note: Pass-through charging requires a charger (not included). Note: To achieve full power for iPad, we recommend using a 45W wall charger.
- Transfer Files in Seconds: Move files to and from your laptop at speeds of up to 5 Gbps via the USB-C and USB-A data ports. Note: The USB C 5Gbps Data port does not support video output.
- HD Display: Connect to the HDMI port to stream or mirror content to an external monitor in resolutions of up to 4K@30Hz. Note: The USB-C ports do not support video output.
- What You Get: Anker 332 USB-C Hub (5-in-1), welcome guide, our worry-free 18-month warranty, and friendly customer service.
Shadow DOM and table boundaries
Ordinary CSS selectors do not cross a Shadow DOM boundary. A selector beginning at the document, such as #results tbody tr, cannot reach a table inside a component’s shadow root. For an open shadow root, use Puppeteer’s supported shadow-root selector syntax or enter the host and query inside the root. Closed shadow roots are not directly queryable from page JavaScript; the component must expose an accessible element or application-level hook.
Also check for nested tables. A descendant selector can accidentally match rows in a table inside a cell. Scope the query to the specific table element before applying the positional part.
Troubleshoot common failures
“failed to find element” or a $eval exception
- Cause: fewer than two matching rows, the wrong table scope, or rows that have not rendered yet.
- Fix: log
await page.$$eval('... tr', rows => rows.length), wait for the first row, and userows[1] ?? nullwhen absence is valid.
The selector returns a header instead of data
- Cause: the query includes
theador counts all rows. - Fix: use
tbody tr:nth-of-type(2)and confirm the DOM section in browser developer tools.
nth-child(2) selects the wrong row
- Cause: another element child changes the child position.
- Fix: use
nth-of-type(2)for the secondtrsibling, or adjust the selector to the actual child structure.
The second row changes between runs
- Cause: sorting, filtering, personalization, pagination, or asynchronous insertion changes row order.
- Fix: set deterministic filters and sort order, wait for the application’s “loaded” state, and use a stable row identifier when you mean a particular record rather than a position.
A click is intercepted or happens too early
- Cause: the row or button is still moving, hidden, covered, or replaced during rendering.
- Fix: use a Locator for the row control, or wait for the fully qualified button selector and verify visibility before clicking.
The query misses a component table
- Cause: the table is inside an open or closed shadow root.
- Fix: use Puppeteer’s shadow-root selector support for an open root; for a closed root, use an exposed component API or accessible surface.
Performance, reliability, and version notes
For one row, a single scoped $eval avoids transferring every row to Node.js. For optional rows or several derived values, one $$eval callback is usually clearer than many round trips. Keep callbacks small and return plain serializable data rather than element handles.
Positional selectors are concise but fragile when the product changes row ordering. If your test or scraper identifies a business record, prefer a stable key in a data attribute or cell value and use position only when position itself is the requirement. Add a clear timeout and capture diagnostics—URL, row count, and a short table excerpt—when a run fails.
Quick wins for a faster PC:
Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →Clear out junk files and repair common Windows errorsFree Scan →Puppeteer documentation pages in the supplied material display versions 25.12.0 for the Page interactions, $eval, and Locator pages, and 25.9.0 for $$eval. Documentation versions can differ, so verify the API against the Puppeteer version installed in your project before relying on version-specific behavior.
Or skip the browser setup
If your goal is a visual snapshot of a table rather than extracting or clicking its second row, ScreenshotNeo can return a screenshot or PDF through one HTTP request. It is separate from Puppeteer DOM selection: it captures the rendered page, while the code above reads elements.
curl -G "https://api.screenshotneo.com/v1/shot"
-d access_key=YOUR_API_KEY
--data-urlencode url=https://stripe.com
-o shot.webp
See the ScreenshotNeo documentation for request parameters. The same request from Python is:
Quick Recap
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)
And in 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}`);
require('fs').writeFileSync('shot.webp', Buffer.from(await res.arrayBuffer()));
ScreenshotNeo removes cookie and consent banners, newsletter popups, and chat widgets before capture; each step can be disabled. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing status. Its MCP server provides take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients. The Free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.
Free tools Windows power users keep installed
One-click scans. No signup required.
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.

