0
# Troubleshooting
1
2
Common issues and solutions when using jsdom.
3
4
## Common Issues
5
6
### Process Won't Exit After Using jsdom
7
8
**Symptoms:** Node.js process hangs after running code with jsdom.
9
10
**Cause:** Timers (setTimeout, setInterval, requestAnimationFrame) keep the event loop alive.
11
12
**Solution:**
13
14
```javascript
15
const dom = new JSDOM(html);
16
17
try {
18
// Your work here
19
const { document } = dom.window;
20
// ...
21
} finally {
22
// Always call close()
23
dom.window.close();
24
}
25
```
26
27
### Relative URLs Fail to Resolve
28
29
**Symptoms:** `<img src="logo.png">` shows as relative URL or fetch fails.
30
31
**Cause:** Default URL is `"about:blank"` which can't resolve relative paths.
32
33
**Solution:**
34
35
```javascript
36
const dom = new JSDOM(html, {
37
url: "https://example.com/page.html" // Provide absolute URL
38
});
39
40
const img = dom.window.document.querySelector("img");
41
console.log(img.src); // "https://example.com/logo.png" (resolved)
42
```
43
44
### Scripts Don't Execute
45
46
**Symptoms:** Inline scripts in HTML don't run, event handlers don't work.
47
48
**Cause:** Default `runScripts` is `undefined`, so no scripts execute.
49
50
**Solution:**
51
52
```javascript
53
// For inline scripts
54
const dom = new JSDOM('<script>/* script */</script>', {
55
runScripts: "dangerously" // Allow inline execution
56
});
57
58
// Or run scripts from outside (safer)
59
const dom2 = new JSDOM(html, { runScripts: "outside-only" });
60
dom2.window.eval("/* your code */");
61
```
62
63
### Resources Don't Load
64
65
**Symptoms:** External stylesheets, scripts, images don't load.
66
67
**Cause:** `resources` option not set (default is no resource loading).
68
69
**Solution:**
70
71
```javascript
72
const dom = new JSDOM(`
73
<link rel="stylesheet" href="styles.css">
74
<script src="app.js"></script>
75
`, {
76
url: "https://example.com/", // Required!
77
resources: "usable" // Enable resource loading
78
});
79
```
80
81
### Layout Properties Return Zero
82
83
**Symptoms:** `offsetWidth`, `getBoundingClientRect()`, etc. return 0.
84
85
**Cause:** jsdom doesn't implement layout calculation.
86
87
**Solution:** Mock the values you need:
88
89
```javascript
90
const element = dom.window.document.querySelector(".box");
91
92
// Mock layout properties
93
Object.defineProperty(element, "offsetWidth", {
94
value: 100,
95
writable: true,
96
configurable: true
97
});
98
99
Object.defineProperty(element, "offsetHeight", {
100
value: 200,
101
writable: true,
102
configurable: true
103
});
104
105
// Now they return your mocked values
106
console.log(element.offsetWidth); // 100
107
console.log(element.offsetHeight); // 200
108
109
// Or create a helper
110
function mockLayout(element, width, height) {
111
Object.defineProperties(element, {
112
offsetWidth: { value: width, writable: true, configurable: true },
113
offsetHeight: { value: height, writable: true, configurable: true },
114
clientWidth: { value: width, writable: true, configurable: true },
115
clientHeight: { value: height, writable: true, configurable: true }
116
});
117
}
118
```
119
120
### Navigation Doesn't Work
121
122
**Symptoms:** Setting `window.location.href` does nothing.
123
124
**Cause:** jsdom doesn't implement navigation.
125
126
**Solution:** Create new JSDOM instances for different pages:
127
128
```javascript
129
// Instead of:
130
// window.location.href = "https://example.com/page2"; // Won't work
131
132
// Do this:
133
async function navigateTo(url) {
134
const newDom = await JSDOM.fromURL(url);
135
return newDom;
136
}
137
138
const page1 = await JSDOM.fromURL("https://example.com/");
139
const page2 = await navigateTo("https://example.com/page2");
140
```
141
142
### Cookies Not Persisting Across Requests
143
144
**Symptoms:** Cookies set in one jsdom aren't available in another.
145
146
**Cause:** Using separate CookieJar instances.
147
148
**Solution:** Share a CookieJar:
149
150
```javascript
151
const { JSDOM, CookieJar } = require("jsdom");
152
153
const cookieJar = new CookieJar(); // Shared across all instances
154
155
const dom1 = new JSDOM(``, {
156
url: "https://example.com/",
157
cookieJar: cookieJar
158
});
159
dom1.window.document.cookie = "session=abc123";
160
161
const dom2 = new JSDOM(``, {
162
url: "https://example.com/",
163
cookieJar: cookieJar // Same jar
164
});
165
166
console.log(dom2.window.document.cookie); // "session=abc123"
167
```
168
169
### "Not Implemented" Errors
170
171
**Symptoms:** Calling certain web APIs throws "not-implemented" errors.
172
173
**Cause:** Some web platform features aren't implemented in jsdom.
174
175
**Solution:** Use try-catch and provide fallbacks:
176
177
```javascript
178
function safeCall(dom, feature, callback) {
179
try {
180
return callback(dom);
181
} catch (error) {
182
if (error.message.includes("not implemented")) {
183
console.warn(`Feature ${feature} not available in jsdom`);
184
return null;
185
}
186
throw error;
187
}
188
}
189
190
const dom = new JSDOM(html);
191
const result = safeCall(dom, "someFeature", (d) => {
192
return d.window.SomeFeature.doSomething();
193
});
194
```
195
196
### Memory Leaks in Long-Running Scripts
197
198
**Symptoms:** Memory usage grows over time when processing many pages.
199
200
**Cause:** Not cleaning up jsdom instances properly.
201
202
**Solution:** Use proper cleanup and consider limits:
203
204
```javascript
205
async function processPages(urls) {
206
for (const url of urls) {
207
let dom = null;
208
try {
209
dom = await JSDOM.fromURL(url);
210
// Process page
211
const data = extractData(dom.window.document);
212
213
// Return data immediately
214
yield data;
215
} finally {
216
// Always cleanup
217
if (dom) {
218
dom.window.close();
219
dom = null;
220
}
221
}
222
223
// Optional: Force garbage collection hint
224
if (global.gc) global.gc();
225
}
226
}
227
228
// Usage
229
for await (const data of processPages(urls)) {
230
console.log(data);
231
}
232
```
233
234
### Text Encoding Issues
235
236
**Symptoms:** Special characters appear as wrong symbols.
237
238
**Cause:** Encoding not specified or detected incorrectly.
239
240
**Solution:** Specify encoding explicitly:
241
242
```javascript
243
// From string (default is UTF-8)
244
const dom1 = new JSDOM('<p>Test: 测试</p>');
245
246
// From Buffer with explicit content type
247
const buffer = fs.readFileSync('page.html');
248
const dom2 = new JSDOM(buffer, {
249
contentType: "text/html; charset=utf-8"
250
});
251
```
252
253
### Attributes with Special Characters
254
255
**Symptoms:** Attributes with colons or special names don't work.
256
257
**Cause:** Some attribute names are namespaced or special.
258
259
**Solution:** Use proper attribute access:
260
261
```javascript
262
const element = document.createElement("div");
263
264
// For standard attributes
265
element.setAttribute("data-value", "123");
266
267
// For attributes with colons or namespaces
268
element.setAttributeNS(
269
"http://example.com/xmlns",
270
"example:attribute",
271
"value"
272
);
273
```
274
275
### Fragment Context Lost
276
277
**Symptoms:** Using `JSDOM.fragment()` then appending doesn't preserve context.
278
279
**Cause:** Fragments have no browsing context.
280
281
**Solution:** Use in proper DOM tree:
282
283
```javascript
284
const frag = JSDOM.fragment('<li>Item</li><li>Item</li>');
285
286
const dom = new JSDOM('<ul id="list"></ul>');
287
const list = dom.window.document.getElementById("list");
288
289
// Append fragment - proper context now
290
list.appendChild(frag);
291
292
console.log(dom.serialize()); // Includes both items
293
```
294
295
## Debugging Tips
296
297
### Enable Node Location Tracking
298
299
```javascript
300
const dom = new JSDOM(html, {
301
includeNodeLocations: true // Enable source mapping
302
});
303
304
const element = dom.window.document.querySelector("p");
305
const location = dom.nodeLocation(element);
306
307
console.log(`Found at line ${location.startLine}, col ${location.startCol}`);
308
```
309
310
### Structured Error Handling
311
312
```javascript
313
async function safeScrape(url, options = {}) {
314
let dom = null;
315
316
try {
317
const fetchOptions = {
318
url,
319
...options
320
};
321
322
dom = await JSDOM.fromURL(url, fetchOptions);
323
324
// Validate response
325
if (!dom.window.document.body) {
326
throw new Error("No body element found in document");
327
}
328
329
return {
330
success: true,
331
dom,
332
error: null
333
};
334
} catch (error) {
335
return {
336
success: false,
337
dom: null,
338
error: {
339
message: error.message,
340
type: error.name,
341
stack: error.stack
342
}
343
};
344
} finally {
345
if (dom) {
346
dom.window.close();
347
}
348
}
349
}
350
351
// Usage
352
const result = await safeScrape("https://example.com");
353
if (result.success) {
354
const { document } = result.dom.window;
355
console.log(document.title);
356
}
357
```
358
359
### Capture jsdom Errors
360
361
```javascript
362
const { VirtualConsole } = require("jsdom");
363
364
const virtualConsole = new VirtualConsole();
365
366
virtualConsole.on("jsdomError", (error) => {
367
console.error(`jsdom error [${error.type}]:`, error.message);
368
369
// Inspect error details
370
console.log("Error details:", error);
371
});
372
373
const dom = new JSDOM(html, { virtualConsole });
374
```
375
376
### Log All Console Output
377
378
```javascript
379
const virtualConsole = new VirtualConsole();
380
381
virtualConsole.on("all", (...args) => {
382
console.log("[jsdom]", ...args);
383
});
384
385
virtualConsole.on("log", (...args) => console.log("[log]", ...args));
386
virtualConsole.on("error", (...args) => console.error("[error]", ...args));
387
virtualConsole.on("warn", (...args) => console.warn("[warn]", ...args));
388
389
const dom = new JSDOM(`
390
<script>
391
console.log("Info");
392
console.error("Error");
393
console.warn("Warning");
394
</script>
395
`, {
396
runScripts: "dangerously",
397
virtualConsole
398
});
399
```
400
401
### Inspect DOM State
402
403
```javascript
404
function inspectDOM(dom) {
405
const { document } = dom.window;
406
407
console.log("Document URL:", document.URL);
408
console.log("Document title:", document.title);
409
console.log("Element count:", document.querySelectorAll("*").length);
410
console.log("Cookies:", dom.window.document.cookie);
411
console.log("LocalStorage:", Object.keys(dom.window.localStorage));
412
console.log("Location:", dom.window.location.href);
413
}
414
415
const dom = new JSDOM(html);
416
inspectDOM(dom);
417
```
418
419
### Verify Resource Loading
420
421
```javascript
422
const { JSDOM, ResourceLoader } = require("jsdom");
423
424
class LoggingResourceLoader extends ResourceLoader {
425
fetch(url, options) {
426
console.log("Fetching:", url);
427
console.log("Options:", options);
428
429
const promise = super.fetch(url, options);
430
431
promise.then(() => {
432
console.log("Success:", url);
433
}).catch((error) => {
434
console.error("Failed:", url, error.message);
435
});
436
437
return promise;
438
}
439
}
440
441
const dom = new JSDOM(`
442
<link rel="stylesheet" href="styles.css">
443
<script src="script.js"></script>
444
`, {
445
url: "https://example.com/",
446
resources: new LoggingResourceLoader()
447
});
448
```
449
450
## Performance Optimization
451
452
### 1. Use JSDOM.fragment() for Simple Parsing
453
454
```javascript
455
// More efficient for simple parsing
456
const frag = JSDOM.fragment('<p>Test</p>');
457
const text = frag.querySelector("p").textContent;
458
```
459
460
### 2. Disable Resource Loading When Not Needed
461
462
```javascript
463
// Faster if you don't need external resources
464
const dom = new JSDOM(html); // Default: no resource loading
465
```
466
467
### 3. Close Unused jsdoms Promptly
468
469
```javascript
470
function processAndClose(html) {
471
const dom = new JSDOM(html);
472
try {
473
const data = extract(dom.window.document);
474
return data;
475
} finally {
476
dom.window.close(); // Free memory immediately
477
}
478
}
479
```
480
481
### 4. Reuse CookieJar for Multiple Pages
482
483
```javascript
484
const cookieJar = new CookieJar();
485
486
// More efficient than creating new jar for each page
487
for (const url of urls) {
488
const dom = new JSDOM(``, { url, cookieJar });
489
// Use and close
490
dom.window.close();
491
}
492
```
493
494
## Performance Monitoring
495
496
### Track Memory Usage
497
498
```javascript
499
const { JSDOM } = require("jsdom");
500
501
function createDOMWithMemoryTracking(html) {
502
const memBefore = process.memoryUsage();
503
504
const dom = new JSDOM(html);
505
const { document } = dom.window;
506
507
// Do work
508
const elements = document.querySelectorAll("*");
509
510
// Track memory after
511
const memAfter = process.memoryUsage();
512
513
const heapUsedMB = (memAfter.heapUsed - memBefore.heapUsed) / 1024 / 1024;
514
console.log(`Memory used: ${heapUsedMB.toFixed(2)}MB`);
515
console.log(`Elements processed: ${elements.length}`);
516
517
return dom;
518
}
519
520
let dom = createDOMWithMemoryTracking('<div>' + '.'.repeat(100000) + '</div>');
521
// Always cleanup
522
dom.window.close();
523
```
524
525
### Batch Processing with Limits
526
527
```javascript
528
async function processPagesWithLimit(urls, options = {}) {
529
const {
530
maxConcurrent = 3,
531
delayBetweenPages = 100,
532
memoryLimitMB = 500
533
} = options;
534
535
const results = [];
536
let currentMemory = process.memoryUsage().heapUsed / 1024 / 1024;
537
538
for (let i = 0; i < urls.length; i += maxConcurrent) {
539
const batch = urls.slice(i, i + maxConcurrent);
540
541
const batchResults = await Promise.all(
542
batch.map(async (url) => {
543
if (currentMemory > memoryLimitMB) {
544
throw new Error(`Memory limit exceeded: ${currentMemory.toFixed(2)}MB`);
545
}
546
547
const dom = await JSDOM.fromURL(url);
548
const result = { url, title: dom.window.document.title };
549
dom.window.close();
550
551
return result;
552
})
553
);
554
555
results.push(...batchResults);
556
currentMemory = process.memoryUsage().heapUsed / 1024 / 1024;
557
558
// Delay between batches
559
if (i + maxConcurrent < urls.length) {
560
await new Promise(r => setTimeout(r, delayBetweenPages));
561
}
562
}
563
564
return results;
565
}
566
```
567
568
## Still Having Issues?
569
570
### Check jsdom Version
571
572
```javascript
573
const { JSDOM } = require("jsdom");
574
console.log(require("jsdom/package.json").version);
575
```
576
577
### Verify Node.js Version
578
579
jsdom requires Node.js v20 or newer:
580
581
```bash
582
node --version # Should be v20.0.0 or higher
583
```
584
585
### Enable Debug Logging
586
587
```javascript
588
// Enable debug logging
589
process.env.DEBUG = "jsdom:*";
590
591
// Or just specific parts
592
process.env.DEBUG = "jsdom:virtualconsole jsdom:*_tough-cookie";
593
```
594
595
### Search for Similar Issues
596
597
Check the [jsdom GitHub Issues](https://github.com/jsdom/jsdom/issues) for your problem.
598
599
### Minimal Reproduction
600
601
Create minimal code that demonstrates the issue:
602
603
```javascript
604
const { JSDOM } = require("jsdom");
605
606
const html = "<!-- minimal HTML that breaks -->";
607
const dom = new JSDOM(html, { /* minimal options */ });
608
609
// Minimal code that demonstrates issue
610
// ...
611
612
console.log("Error: [your issue]");
613
```
614