For reliable repeated work, create a new Nightmare() instance for every run, queue that run’s actions, finish with .end(), and await the returned promise before creating the next instance. .end() completes the queue and closes that run’s Electron process, so an ended instance must not be reused. The sequence below is the safe baseline for Node.js scripts and services.
The repeatable Nightmare.js lifecycle
Nightmare is a Node.js browser-automation module built on Electron. One instance owns one action queue and one Electron process. Treat that instance as single-use: configure it, perform one unit of work, call .end(), wait for completion, then discard it.
The project README documents .end() as completing queued operations, disconnecting, and closing the Electron process (Nightmare README). Starting the next run only after the previous promise settles prevents queues from being mixed and avoids attempting actions on a closed browser.
A minimal sequential runner
const Nightmare = require('nightmare');
async function runOnce(url) {
const nightmare = Nightmare();
try {
return await nightmare
.goto(url)
.evaluate(() => document.title)
.end();
} catch (error) {
// Let the caller decide whether to retry, log, or stop.
throw error;
}
}
async function main() {
for (const url of ['https://example.com', 'https://example.org']) {
const title = await runOnce(url);
console.log(`${url}: ${title}`);
}
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
There are three important details in this example:
runOnceconstructsNightmare()inside the function, so every call receives a fresh browser.- All actions for that browser remain in one chain. The chain ends with
.end(). - The loop awaits
runOnce. The second Electron process is not started until the first run has completed and closed.
Install Nightmare and check your runtime
Install the module in your project with npm:
npm install --save nightmare
The npm listing identifies version 3.0.2 and says it was published years ago; that is historical package information, not a promise of current Node.js or operating-system compatibility (npm package listing). Check what your project actually resolved:
Free tools Windows power users keep installed
One-click scans. No signup required.
#1 Best Overall
npm ls nightmare
node --version
npm --version
Validate the installed version against the Node.js runtime and operating system you intend to deploy. Electron can require UI-related libraries that are absent from minimal server distributions. A successful npm install therefore does not guarantee that Electron can launch on your host.
Run many jobs in order
Put each URL or task in data, and keep the lifecycle boundary in one function. This pattern gives every job independent page state and makes a failed job visible to the caller.
const Nightmare = require('nightmare');
async function captureTitle(url) {
const browser = Nightmare();
return browser
.goto(url)
.wait('body')
.evaluate(() => ({
title: document.title,
href: location.href
}))
.end();
}
async function processUrls(urls) {
const results = [];
for (const url of urls) {
try {
results.push({ url, ok: true, data: await captureTitle(url) });
} catch (error) {
results.push({
url,
ok: false,
error: error instanceof Error ? error.message : String(error)
});
}
}
return results;
}
processUrls(['https://example.com', 'https://example.org'])
.then(console.log)
.catch(console.error);
Here, .wait('body') is only an example of a page readiness condition. Replace it with a selector that is meaningful for your page, or remove it when navigation itself is sufficient. Keep the error boundary outside captureTitle if you want to continue processing later URLs after one fails.
Repeating the same URL
Calling captureTitle('https://example.com') repeatedly still creates a new instance each time. Do not move const browser = Nightmare() outside the function unless you deliberately want one long-lived queue and process. For independent runs, a fresh instance is the documented usage model.
Recommended Free Tools
Rank #2
What happens when a run fails?
Nightmare operations are queued and are resolved through the promise returned by the chain. Attach your error handling to that promise, as in the examples. A rejection means the caller receives a failure instead of silently treating the run as successful.
Do not continue issuing actions after .end(). The method closes the Electron process; create another instance for a retry. If you implement retries, put the retry loop around runOnce, not around an already-ended object.
async function withRetry(url, attempts = 2) {
let lastError;
for (let attempt = 1; attempt <= attempts; attempt += 1) {
try {
return await runOnce(url);
} catch (error) {
lastError = error;
if (attempt === attempts) throw lastError;
}
}
}
This example deliberately creates a new browser on each attempt. Add logging and a backoff appropriate to your application; the Nightmare documentation does not establish a universal retry policy.
Choose whether browser state should persist
By default, each Nightmare instance uses an in-memory Electron partition. Cookies, localStorage, and other persistent browser state disappear when that instance ends. This is the right default when jobs must be isolated from one another.
Rank #3
Isolated runs (default)
function isolatedBrowser() {
return Nightmare();
}
Use this for tests, unrelated customer accounts, or jobs where a prior login must never affect the next page.
Shared state across instances
If several runs intentionally share cookies or localStorage, configure the same Electron webPreferences.partition value for every instance. A partition beginning with persist: stores the state rather than using an in-memory session, as shown in the README (Nightmare README).
const Nightmare = require('nightmare');
function sessionBrowser() {
return Nightmare({
webPreferences: {
partition: 'persist:my-session'
}
});
}
async function visitWithSharedSession(url) {
const browser = sessionBrowser();
return browser
.goto(url)
.evaluate(() => ({ title: document.title, cookies: document.cookie }))
.end();
}
Use a stable, intentionally chosen partition name only for jobs that are meant to share a session. Do not use one global partition for unrelated users: cookies and storage from one workflow can then be visible to another. A persistent partition also changes cleanup expectations; remove or reset the profile using your operating system and deployment strategy when the session must be revoked.
Sequential execution versus concurrency
Sequential execution is the conservative choice: one Electron process is created and closed at a time, memory use is easier to reason about, and failures map cleanly to individual jobs.
Rank #4
If you need parallelism, still create a distinct instance per task:
async function runInParallel(urls) {
return Promise.all(urls.map((url) => runOnce(url)));
}
The available Nightmare documentation describes instance creation and shutdown but does not provide a general performance limit or a guarantee that launching many Electron instances concurrently is safe. Treat concurrency as an application-specific load test. Start with a small limit, monitor memory and process counts, and reduce the limit if the host becomes unstable. Never share one Nightmare instance between concurrent jobs; its action queue and browser state would be interleaved.
Troubleshooting repeated runs
| Symptom | Likely cause | Fix |
|---|---|---|
Cannot read properties ... or actions fail after the first job |
The code is reusing an instance after .end(), or multiple jobs are writing to one queue. |
Create Nightmare() inside the per-run function and await its .end() promise before starting another run. |
| Cookies or login disappear on every run | Each default in-memory partition is intentionally discarded when the instance ends. | Configure the same webPreferences.partition beginning with persist: when shared state is required. |
| One job sees another job’s account or storage | Several instances use the same persistent partition. | Give unrelated workflows different partition names, or return to the default in-memory partition. |
| Install succeeds but Electron will not launch on a server | The server image may lack UI-related libraries required by Electron. | Check the operating system’s Electron dependencies, install the required libraries for that distribution, and verify the runtime in the same environment where Node runs. The Nightmare documentation specifically warns that server distributions can lack these dependencies. |
| A navigation or selector step rejects | The target URL failed to load, the selector never appeared, or the page differs from the assumption in the script. | Log the URL and failing step, verify the page manually, use a selector that exists after navigation, and retry by creating a completely new instance. |
| Parallel jobs exhaust memory or leave unstable processes | Each instance owns an Electron process; the documentation does not define a safe concurrency level. | Use sequential processing or a small concurrency limit, then measure resource use on your deployment host. |
Operational guidance for production scripts
- Keep the boundary obvious: one function creates, uses, and ends one instance.
- Return useful data: resolve with the title, extracted fields, or a structured result so callers can distinguish success from an empty value.
- Log context: include the URL, job identifier, attempt number, and error message before retrying.
- Prefer isolation by default: shared partitions are a deliberate session design, not a performance switch.
- Plan for startup overhead: a fresh instance means a fresh Electron process. Sequential runs trade some throughput for predictable isolation; concurrency trades shorter wall-clock time for higher resource demand.
- Test the deployed environment: Electron dependencies, Node version, and operating-system packaging can differ between a laptop and a server.
Or skip the browser setup
If your goal is simply to obtain website screenshots rather than automate an interactive Electron session, ScreenshotNeo provides a single HTTP request. It accepts cookie and consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each cleanup step can be disabled. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the result with X-Page-Verdict and X-Billed headers. Its MCP server exposes take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients.
See the ScreenshotNeo API documentation for all options. A basic Node.js request is:
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
const q = new URLSearchParams({
access_key: 'YOUR_API_KEY',
url: 'https://example.com'
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot failed: ${res.status}`);
const image = Buffer.from(await res.arrayBuffer());
await require('fs').promises.writeFile('shot.webp', image);
The equivalent cURL command is:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com -o shot.webp
Python:
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://example.com"},
timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
ScreenshotNeo also supports full-page captures with lazy images loaded, CSS-selector element shots, dark mode, device presets or custom viewports, retina scale, PDF output, custom CSS and JavaScript, clicks before capture, selector waits, delays, network-idle waits, request and resource blocking, custom headers/cookies/user agents, authorization, timezone and geolocation, transparent backgrounds, resizing, chosen cache TTLs, signed image links, asynchronous jobs with signed webhooks, bulk capture of up to 100 URLs per call, a usage API, and an OpenAPI specification. Common parameter names used by other screenshot APIs are accepted to ease migration.
The Free plan includes 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 screenshots; Growth is $15 for 15,000, Pro $39 for 60,000, Scale $99 for 250,000, and Business $249 for 1,000,000. Every feature is on every plan, and yearly billing provides two months free. Sign up for the free 1,000-screenshot plan to try it without a card.
Frequently Asked Questions
Is Nightmare.js version 3.0.2 guaranteed to work with my Node.js release?
No. The npm listing’s 3.0.2 information is dated package context. Check the version installed with npm ls nightmare and validate it on your exact Node.js version and operating system.
Can a persistent partition be used by unrelated applications?
It should be shared only by workflows that intentionally need the same browser state. Use different partition names, or the default in-memory storage, to keep accounts and cookies isolated.
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 reinstallCrashes, No Sound, or Screen Glitches?
Random freezes, missing sound and display glitches usually trace back to one bad driver. Find and replace yours safely.Free scan · under a minuteDoes running jobs in parallel have a documented safe limit?
No general limit is established in the Nightmare documentation. Use separate instances, begin with low concurrency, and measure Electron process and memory usage on your host.
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.

