Puppeteer saves a screenshot only when you pass a path to page.screenshot() (or an element’s screenshot() method). A relative path is resolved from Node.js’s current working directory, not from a Puppeteer-specific screenshots folder. If you omit path, Puppeteer returns image data in memory instead of creating a file.
The rule that determines the destination
The path option is the complete destination filename. For example:
await page.screenshot({ path: 'screenshots/home.png' });
If the process was started in /work/app, that relative path points to /work/app/screenshots/home.png. The base is the value returned by process.cwd() at runtime. It can differ from the directory containing your JavaScript file when you launch the script from another directory, a test runner, an IDE, a Docker container, or a process manager.
Puppeteer does not silently choose a default directory or filename. The documented behavior is:
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
#1 Best Overall
- A relative
pathis resolved against the current working directory. - No
pathmeans no disk write; the screenshot is returned to your code. - The filename extension is used to infer the image format when you do not provide
type.
Relative paths: convenient, but dependent on launch location
Relative paths are useful for a small script or a project whose working directory is fixed:
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'networkidle2' });
await page.screenshot({ path: 'screenshots/example.png' });
await browser.close();
Run this from the project directory and the output will be under that directory. Run it from elsewhere, and the same string points elsewhere. To see the directory before capturing, log it:
console.log('working directory:', process.cwd());
A common “missing screenshot” report is therefore a successful capture saved in a different directory than the developer expected. Search from the printed working directory, or use an absolute destination.
Absolute paths: deterministic output
An absolute path removes ambiguity caused by the launch directory. Build it with Node’s path utilities and create the parent directory before calling Puppeteer:
Quick wins for a faster PC:
Scan for outdated or missing drivers - takes under a minuteDriver Scan →Clear out junk files and repair common Windows errorsFree Scan →Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →import fs from 'node:fs/promises';
import path from 'node:path';
import puppeteer from 'puppeteer';
const outputDir = path.resolve(process.cwd(), 'artifacts');
await fs.mkdir(outputDir, { recursive: true });
const output = path.join(outputDir, 'home.png');
const browser = await puppeteer.launch();
try {
const page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'networkidle2' });
await page.screenshot({ path: output });
console.log(`Saved screenshot to ${output}`);
} finally {
await browser.close();
}
path.resolve() turns the base into an absolute location, while fs.mkdir(..., { recursive: true }) makes missing parent directories. The screenshot path option identifies the file; do not rely on it to create a directory tree for you.
You can anchor output to the module location instead of the launch directory when that is what your deployment needs. In an ES module, convert import.meta.url to a filesystem path, then join your artifact directory. In CommonJS, __dirname provides the module directory. Choose one anchor deliberately: a working-directory-based path is often better for CI artifacts, whereas a module-relative path can be better for an application’s bundled assets.
Rank #2
- HTML CSS Design and Build Web Sites
- Comes with secure packaging
- It can be a gift option
What is saved, and what stays in memory?
| Call pattern | Destination | Result | Useful when |
|---|---|---|---|
page.screenshot({ path: 'shots/a.png' }) |
Relative file under process.cwd() |
Image is written to disk | The launch directory is controlled |
page.screenshot({ path: '/var/tmp/a.png' }) |
Exact absolute filename | Image is written to disk | Workers, containers, and CI need a stable location |
const bytes = await page.screenshot() |
No file | A Uint8Array is returned |
Uploading directly to object storage or processing in memory |
await page.screenshot({ encoding: 'base64' }) |
No file unless you write one yourself | A base64 string is returned | Embedding in a JSON response or data URL |
For an in-memory upload, pass the returned bytes to your storage SDK rather than adding a temporary path. If another API requires base64, request encoding: 'base64'; otherwise the normal return is binary data.
Choose the output scope before choosing the filename
Viewport screenshot
The default captures the currently visible viewport:
await page.screenshot({ path: 'artifacts/viewport.png' });
The path controls where the file goes; viewport dimensions come from the page or from an explicit viewport set with page.setViewportSize-equivalent configuration during page setup.
Full-page screenshot
Set fullPage: true when the image should include the document beyond the visible viewport. The default is false:
await page.screenshot({
path: 'artifacts/full-page.png',
fullPage: true
});
This changes the captured area, not the destination. Very tall pages can require substantially more memory and encoding time than a viewport capture. If a page loads content only after scrolling, wait for the relevant content or trigger the page’s loading behavior before capturing.
One element
Use an element handle when you need a card, chart, or other component rather than the whole page:
Rank #3
const card = await page.waitForSelector('.card');
if (!card) throw new Error('The .card element was not found');
await card.screenshot({ path: 'artifacts/card.png' });
ElementHandle.screenshot() attempts to scroll a hidden element into view before capturing it. A missing selector still fails your script, so wait for a selector that identifies the final rendered element and handle a null result explicitly.
Format and image controls
When path is present, Puppeteer infers the image type from the filename extension. Use a matching extension such as .png, .jpeg, or .webp. If the format must not depend on a filename, set type explicitly:
await page.screenshot({
path: 'artifacts/hero-image',
type: 'webp',
quality: 82
});
Quality applies to lossy formats such as JPEG and WebP. Other documented screenshot controls include:
clipfor a rectangular region of the page.omitBackground: trueto preserve transparency where the page and format support it.captureBeyondViewportto control capture outside the current viewport for supported capture modes.encodingto choose binary output or base64 when you are not writing directly to a path.
Keep the extension and type consistent. A mismatch can confuse downstream image viewers and pipelines even when the capture itself succeeds.
The Tool Desk
Outbyte Driver Updater FREEScan for outdated or missing drivers - takes under a minuteDriver Scan →Outbyte PC Repair FREEClear out junk files and repair common Windows errorsFree Scan →A complete reusable capture function
This example makes the destination explicit, creates its directory, waits for the page, and always closes the browser:
import fs from 'node:fs/promises';
import path from 'node:path';
import puppeteer from 'puppeteer';
export async function capture(url, filename = 'page.png') {
const directory = path.resolve(process.cwd(), 'artifacts');
await fs.mkdir(directory, { recursive: true });
const destination = path.join(directory, filename);
const browser = await puppeteer.launch();
try {
const page = await browser.newPage();
await page.setViewport({ width: 1440, height: 900 });
await page.goto(url, { waitUntil: 'networkidle2', timeout: 30_000 });
await page.screenshot({ path: destination, fullPage: true });
return destination;
} finally {
await browser.close();
}
}
const savedAt = await capture('https://example.com', 'example-full.png');
console.log(savedAt);
Use a unique filename when several workers capture at once. A timestamp, job ID, or URL hash prevents one job from overwriting another. Keep the artifact directory outside temporary browser profiles if you need to collect it after the process exits.
Rank #4
- Brand: Wiley
- Set of 2 Volumes
- A handy two-book set that uniquely combines related technologies Highly visual format and accessible language makes these books highly effective learning tools Perfect for beginning web designers and front-end developers
Troubleshooting: why the file is missing or unusable
The file exists, but not where expected
Cause: the path was relative and the process started in another directory. Fix: print process.cwd(), inspect that directory, or switch to path.resolve() and log the final absolute filename.
ENOENT or a “no such file or directory” error
Cause: the parent directory does not exist. Fix: call fs.mkdir(parent, { recursive: true }) before screenshot(). Do not assume a repository’s empty folder or an ignored CI directory will be present in a fresh checkout.
EACCES, permission denied, or read-only filesystem
Cause: the Node process cannot write to the selected location, which is common in restricted containers and serverless runtimes. Fix: choose a writable temporary or mounted artifact directory, verify ownership and permissions, and use the returned bytes for an upload when local persistence is not available.
page.screenshot() returned bytes instead of a PNG file
Cause: no path was supplied. Fix: add a destination, or intentionally keep the bytes in memory and upload them. A screenshot call without path is not a failed save; it is the in-memory API.
The image is only the visible portion
Cause: fullPage defaults to false. Fix: set fullPage: true, or use an element screenshot when a single component is the required scope.
The element capture fails or is blank
Cause: the selector did not match, the element was rendered later, or its content depends on a state that was not reached. Fix: wait for a specific selector, wait for the relevant network or application state, verify the element’s dimensions, and capture only after it is visible and populated.
Windows Errors? Fix Them Before They Spread
Repair common Windows errors and clear accumulated junk for a smoother, more stable PC - no reinstall needed.Free scan · no reinstallOutdated 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
The format is wrong
Cause: the extension inferred a different type than your pipeline expects, or the explicit type conflicts with the filename. Fix: use a matching extension and, when necessary, set both type and quality deliberately.
Reliability and performance choices
- Reuse a browser for a batch of pages, but create a fresh page per capture so cookies, viewport state, and DOM changes do not leak between jobs.
- Use an explicit navigation timeout and a wait condition appropriate to the site. “Network idle” is not a guarantee that client-rendered data or animations have finished.
- Prefer element or clipped captures when a full document is unnecessary; they reduce image dimensions and encoding work.
- For full-page captures, test unusually long documents and pages with fixed-position headers. Large images consume memory both in Chromium and in Node while they are encoded or uploaded.
- Write to a unique temporary filename and rename it after a successful capture if consumers must never see a partial file.
- Record the resolved path, URL, viewport, format, and capture error in job logs. That makes a path problem distinguishable from a navigation or rendering problem.
Or skip the browser setup
If you only need a clean website image or PDF, ScreenshotNeo is a hosted alternative to maintaining Chromium, writable directories, and navigation waits. It accepts one GET request and returns a PNG, JPEG, WebP, or PDF. Before capture it accepts the cookie or consent banner like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each cleanup step can be turned off.
Only clean shots are billed. Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and each response identifies the result with X-Page-Verdict and X-Billed headers. ScreenshotNeo also provides an MCP server for Claude, Cursor, and other MCP clients with take_screenshot, get_page_info, and capture_pdf tools.
See the ScreenshotNeo API documentation for the complete parameter list. A direct cURL request is:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
The equivalent Python request is:
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)
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(`ScreenshotNeo returned ${res.status}`);
const data = Buffer.from(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', data));
Beyond the URL and output format, the service exposes 63 options, including full-page capture with lazy images loaded, CSS-selector element capture, dark mode, 12 device presets and arbitrary viewports, retina scale, PDF paper size/margins/landscape/page ranges, HTML/CSS-to-image, custom CSS and JavaScript, pre-capture clicks, hidden selectors, waits for a selector or delay or network idle, blocking ads/trackers/requests/resource types, custom headers/cookies/user agent/Authorization, timezone and geolocation, transparent backgrounds, image resizing, selectable cache TTLs, signed links for public <img> tags, asynchronous jobs with signed webhooks, bulk capture of up to 100 URLs per call, a usage API, and an OpenAPI specification. Parameter names used by other screenshot APIs also work, which can simplify migration. Every feature is included on every plan.
| Plan | Included shots | Price |
|---|---|---|
| Free | 1,000 per month | $0, no card |
| Starter | 3,000 | $5 |
| Growth | 15,000 | $15 |
| Pro | 60,000 | $39 |
| Scale | 250,000 | $99 |
| Business | 1,000,000 | $249 |
Yearly billing gives two months free. You can start with 1,000 free screenshots a month with no card, then move to paid plans starting at $5 for 3,000 shots when a hosted capture endpoint fits your workflow.
Frequently Asked Questions
Does setting fullPage change where Puppeteer writes the image?
No. It changes the captured area only. The path value still determines the filename and directory.
Can I force a format when the filename has no extension?
Yes. Pass an explicit type such as 'webp' (and a suitable quality value for lossy formats) instead of relying on extension inference.
Do these 3 things before closing this tab:
1Scan for outdated or missing drivers - takes under a minute2Clear out junk files and repair common Windows errors3Fix the driver behind crashes, sound loss and screen glitchesQuick 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.

