Do these 3 things before closing this tab:
1Repair Windows errors before they cause bigger problems2Scan for outdated or missing drivers - takes under a minute3Clear out junk files and repair common Windows errorsLoad the HTML into Cheerio, select the <title> element, and read its text:
import * as cheerio from 'cheerio';
const $ = cheerio.load(html);
const title = $('title').text().trim();
The trim() call removes indentation and newlines that may surround the title in the source. If the result is empty, check whether a <title> element was received and whether the page creates it later with client-side JavaScript.
The basic Cheerio title lookup
cheerio.load(html) accepts a document string and returns the $ function used to query it. The CSS selector title matches the document title, and .text() reads the text inside that element.
import * as cheerio from 'cheerio';
const html = `<!doctype html>
<html>
<head>
<title>Product documentation</title>
</head>
<body><h1>Docs</h1></body>
</html>`;
const $ = cheerio.load(html);
const title = $('title').text().trim();
console.log(title); // Product documentation
Use .trim() whenever the title will be displayed, compared, stored, or sent in an API response. Cheerio preserves whitespace from the source, so a neatly indented document can otherwise produce a value containing newlines or extra spaces.
#1 Best Overall
Get the HTML before querying it
Cheerio parses markup that you already have; it does not, by itself, behave like a visual browser. A typical extraction therefore has two separate stages: retrieve the response body, then parse that body.
Fetch a page with Node.js
import * as cheerio from 'cheerio';
const response = await fetch('https://example.com');
if (!response.ok) {
throw new Error(`HTTP ${response.status} ${response.statusText}`);
}
const html = await response.text();
const $ = cheerio.load(html);
const title = $('title').text().trim();
console.log({ title });
Keep the retrieval and parsing steps separate when troubleshooting. If the title is missing, first inspect the exact response body that was passed to Cheerio; a redirect, access-denied page, or application shell may not be the page you expected.
Wrap extraction in a reusable function
import * as cheerio from 'cheerio';
export function getTitle(html) {
const $ = cheerio.load(html);
const selection = $('title');
return {
found: selection.length > 0,
title: selection.text().trim(),
html: $.html()
};
}
const result = getTitle('<html><head><title> Hello </title></head></html>');
console.log(result.found); // true
console.log(result.title); // Hello
Checking selection.length distinguishes “there was no matching element” from “the title element exists but contains no characters.” Calling .text() on an empty selection is safe: it returns an empty string rather than throwing.
Choose the loader that matches your input
Cheerio provides different loaders for strings, bytes, streams, and direct URL retrieval. Select one based on the form and encoding of the data you actually receive.
| Loader | Use it when | Important detail |
|---|---|---|
load(html) |
You have a decoded HTML string. | Returns the $ query function immediately. |
loadBuffer(buffer) |
You have raw bytes and are not certain of the encoding. | Cheerio can sniff the source encoding before parsing. |
stringStream |
You are streaming text that is already decoded. | Feed decoded chunks and read the parsed document when the stream finishes. |
decodeStream |
You are streaming raw bytes with unknown encoding. | Decoding is handled while the byte stream is consumed. |
fromURL(url) |
You want Cheerio to fetch a URL asynchronously. | Use await; the result is the same query-style interface. |
Parse bytes when encoding may be uncertain
import * as cheerio from 'cheerio';
const response = await fetch('https://example.com');
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const bytes = Buffer.from(await response.arrayBuffer());
const $ = cheerio.loadBuffer(bytes);
const title = $('title').text().trim();
console.log(title);
Using a byte-oriented loader avoids prematurely decoding content with the wrong character set. This matters when titles contain accented characters or non-Latin scripts and the server’s encoding is not what your HTTP client assumed.
Let Cheerio fetch a URL
import * as cheerio from 'cheerio';
const $ = await cheerio.fromURL('https://example.com');
const title = $('title').text().trim();
console.log(title);
fromURL is convenient for a straightforward request. Use your own HTTP client instead when you need explicit control over authentication, redirects, retries, proxy settings, or response inspection before parsing.
Why $('title').text() can be empty
There is no title element in the received markup
Cheerio returns an empty selection when no element matches. Verify both the count and the document that was parsed:
const $ = cheerio.load(html);
console.log('matches:', $('title').length);
console.log('value:', JSON.stringify($('title').text()));
console.log($.html());
A zero count means the response did not contain a matching <title>. Logging $.html() (or a bounded slice of it in production logs) helps reveal whether you received an error page, a redirect destination, a truncated response, or a document fragment.
Free tools Windows power users keep installed
One-click scans. No signup required.
Rank #3
The element exists but contains only whitespace
.text() preserves source whitespace. An indented title can therefore look blank in logs even though it contains line breaks and spaces. Compare the raw and normalized values:
const raw = $('title').text();
const normalized = raw.trim();
console.log({ raw: JSON.stringify(raw), normalized });
If your application needs internal spacing normalized as well, apply a policy after extraction, such as replacing runs of whitespace with a single space. Do that only when it fits your data requirements; trimming outer whitespace is the non-destructive default.
The title is inserted by JavaScript
Cheerio does not execute scripts. A React, Vue, or other client-side application may send an initial document without a title and add one after JavaScript runs in a browser. Parsing that initial response cannot recover content that was never present in it.
Use a browser automation tool such as Puppeteer or Playwright to load the page, wait for the application to render, obtain the resulting HTML, and then pass that HTML to Cheerio:
import { chromium } from 'playwright';
import * as cheerio from 'cheerio';
const browser = await chromium.launch();
const page = await browser.newPage();
try {
await page.goto('https://example.com/app', { waitUntil: 'networkidle' });
const renderedHtml = await page.content();
const $ = cheerio.load(renderedHtml);
console.log($('title').text().trim());
} finally {
await browser.close();
}
The browser performs the JavaScript execution; Cheerio still performs the final, convenient DOM query. If the application sets the title only after a user action or an API response, wait for a selector or other application-specific condition before calling page.content().
Handling unusual documents
Multiple title elements
Well-formed HTML normally has one document title, but malformed markup or fragments can contain more than one match. .text() concatenates text from all matched elements. Detect this explicitly if one title is required:
const titles = $('title');
if (titles.length === 0) {
throw new Error('No title element found');
}
if (titles.length > 1) {
console.warn(`Found ${titles.length} title elements; using the first`);
}
const title = titles.first().text().trim();
Fragments and incomplete responses
Cheerio can parse fragments, but a fragment that does not include <title> cannot yield a document title. Confirm that your HTTP client read the complete body and that any upstream size limit did not cut off the head section before parsing.
Case and selector behavior
Use the CSS selector title exactly as shown. Cheerio parses HTML documents and normalizes them for traversal, so querying the element by its tag is preferable to searching the raw response with a regular expression.
Recommended Free Tools
Best Value
Troubleshooting checklist
| Symptom | Likely cause | Fix |
|---|---|---|
'' with no exception |
No matching element, or the element has no text. | Inspect $('title').length and $.html(). |
| Title contains newlines | Source indentation is preserved. | Call .trim(); normalize internal whitespace only if required. |
| Expected title is absent on a single-page app | JavaScript creates it after the initial response. | Render with Puppeteer or Playwright, then parse the rendered HTML. |
| Characters are garbled | Bytes were decoded with the wrong encoding. | Use loadBuffer or decodeStream for raw bytes. |
| Unexpected title from a request | Redirect, block page, login page, or server error was returned. | Check status, final URL, headers, and the body before loading it. |
| Intermittent network failures | Timeouts or transient upstream errors occurred before parsing. | Add bounded timeouts, retries with backoff, and logging around retrieval; do not retry parse errors as if they were network errors. |
Or skip the browser setup
If your real goal is a rendered screenshot or PDF rather than the title string, ScreenshotNeo makes the capture a single request. It accepts a URL, handles the browser session, and returns a PNG, JPEG, WebP, or PDF. The API is not a replacement for Cheerio when you must store the title text, but it avoids maintaining browser automation for visual captures.
For example, this cURL request captures a page as WebP:
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 API documentation for request parameters and response details. Equivalent clients are:
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(`HTTP ${res.status}`);
const fs = await import('node:fs/promises');
await fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));
ScreenshotNeo removes cookie and consent banners, newsletter popups, and chat widgets before capture; each cleanup 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 result with X-Page-Verdict and X-Billed. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools to Claude, Cursor, and other MCP clients.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Plans include 1,000 screenshots per month free without a card; paid plans start at $5 for 3,000 shots. Every plan includes the available features, including full-page and lazy-image capture, CSS-selector element capture, device and retina settings, PDF controls, custom CSS and JavaScript, click and wait actions, request blocking, headers, cookies, user-agent, authorization, timezone and geolocation, transparent backgrounds, resizing, configurable caching, signed links, asynchronous jobs with signed webhooks, bulk capture of up to 100 URLs per call, usage reporting, and an OpenAPI specification. Parameters used by other screenshot APIs are also accepted to ease migration.
Create a free ScreenshotNeo account to use the 1,000 monthly screenshots with no card, or choose a paid plan when your capture volume requires 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.

