Handling Navigation Errors in Puppeteer
A guide to handling navigation errors and related corner cases in Puppeteer.
Important!
The post is not about all possible errors that can happen in Puppeteer, but only about the errors that happen when navigating websites.
Puppeteer is a powerful Node.js library for controlling Chrome, Chromium, or any other browser that supports the Chrome DevTools Protocol.
If you have ever run into networking or timeout issues, you know how frustrating it can be when your automation fails silently or gets stuck.
But imagine you are running a screenshot rendering service like I do, and your cluster networking fails, but your customers still get successful screenshots like this:
The Puppeteer was returning a successful response with status code 200 OK
, but the rendered page was a default browser error page.
That was a really painful event! But I can’t blame Puppeteer for that - it was a combination of error handling techniques I used that eventually led to confusion.
But now when it works well, I want to share with you my lessons on error handling when navigating websites with Puppeteer.
Handling errors
- Puppeteer times out when navigating to a website.
- Networking issues including DNS issues.
- HTTP errors returned by the website.
- Handling default browser error pages.
Puppeteer times out when navigating to a website
Puppeteer will throw a TimeoutError if the navigation takes too long, meaning the page did not load within the specified limit. Websites with large resources or slow responses can trigger this, or if your waitUntil
conditions are never met.
To handle:
- Use a sensible timeout that balances speed and reliability.
- Check whether a partial load is enough (e.g.,
domcontentloaded
) or you want full resource loading (load
,networkidle0
). - Consider catching TimeoutError and deciding if you will retry, abort or even continue rendering anyway.
let response = null;
try {
// set a relatively short timeout of 5 seconds
response = await page.goto("https://example.com", {
timeout: 5000,
waitUntil: "domcontentloaded",
});
} catch (error) {
if (error instanceof TimeoutError) {
console.log("Navigation timed out, but continuing anyway...");
// you often still work with the partially loaded page, but you must be sure that the page is not broken
// that the response is successful and the returned page is not default browser error page
} else {
// re-throw other errors
throw error;
}
}
if (
response &&
response.status() === 200 &&
page.url().startsWith("chrome-error://")
) {
// execute your logic
}
Networking issues including DNS issues
You cluster networking might fail, a DNS server might be down, or a website might be temporarily unavailable. There are many reasons why networking might fail.
Often page.goto()
will throw an error, and you can even catch it and check the error message:
try {
await page.goto("https://example.com");
} catch (error) {
if (error.message && error.message.startsWith("net::ERR")) {
// networking error occurred
}
}
But sometimes the error is not a networking error, but a timeout error or a page load error. And Puppeteer still returns a response with a status code 200 OK
.
Make sure to check page URL to determine if the page was loaded successfully:
if (page.url().startsWith("chrome-error://")) {
// you can just throw new Error("Page load failed, but a default browser error page was returned");
// or you can try to detect errors in the page content
const content = await page.content();
if (content.includes("ERR_NAME_NOT_RESOLVED")) {
// handle the error
} else if (content.includes("ERR_INTERNET_DISCONNECTED")) {
// handle the error
} else {
// handle the error
}
}
HTTP errors returned by the website
Websites can return HTTP errors like 404 Not Found
or 500 Internal Server Error
.
You can catch these errors and handle them accordingly:
const response = await page.goto("https://example.com");
if (!response) {
// sometimes the response is not returned immediately, so we need to wait for it
response = await page.waitForResponse(() => true, { timeout: 5000 });
}
if (response.status() === 404) {
// handle the error
} else if (response.status() === 500) {
// handle the error
} else {
// handle the error
}
Make sure to wait for the response, it is not often returned immediately.
Handling default browser error pages
And even if everything is fine and successful, make sure to check the page URL to determine if the page was loaded successfully:
if (page.url().startsWith("chrome-error://")) {
// handle the error
}
Complete code example
I know the code below is a bit ugly, you can improve it if want, but the idea was to demonstrate all the error handling techniques and possible corner cases when navigating websites with Puppeteer.
Combining all the techniques, we get the following code:
const puppeteer = require("puppeteer");
(async () => {
let browser;
try {
browser = await puppeteer.launch({
headless: true,
});
const page = await browser.newPage();
let response;
try {
// try navigation with a short timeout
response = await page.goto("https://example.com", {
timeout: 5000, // 5 seconds
waitUntil: "domcontentloaded",
});
} catch (error) {
// handle navigation and timeout errors
if (error instanceof puppeteer.errors.TimeoutError) {
// you might decide to re-try, throw, or move on.
// for demonstration, we continue with the partially loaded page.
} else if (error.message && error.message.startsWith("net::ERR")) {
// networking error (DNS, connection, etc). handle or re-try if needed.
// e.g., throw error;
} else {
// an unexpected navigation error occurred
throw error;
}
}
// if we did get a response, check HTTP status codes
if (response) {
const status = response.status();
if (status >= 400) {
// handle the error
}
} else {
// no initial response object was returned, try waiting for a response
try {
const waitedResponse = await page.waitForResponse(() => true, {
timeout: 5000,
});
const waitedStatus = waitedResponse.status();
if (waitedStatus >= 400) {
// handle the error
}
} catch (waitError) {
// handle the error
}
}
// check if the page fell back to a default browser error page
if (page.url().startsWith("chrome-error://")) {
// optionally inspect page content for the specific reason
const content = await page.content();
if (content.includes("ERR_NAME_NOT_RESOLVED")) {
// handle the error
} else if (content.includes("ERR_INTERNET_DISCONNECTED")) {
// handle the error
} else {
// handle the error
}
// decide how to handle: throw, return, etc.
// for demonstration, we will return early.
return;
}
// no you can try to perform your actions
} catch (generalError) {
// handle the error
} finally {
// close the browser if it was opened
if (browser) {
await browser.close();
}
}
})();
Final thoughts
In complex web environments, navigation might fail for many reasons: DNS issues, server errors, slow responses, or default browser error pages.
The goal is to ensure that Puppeteer doesn’t fail silently and you catch all possible corner cases.
Feel free to adapt the snippet above to log, retry, or gracefully handle other specific scenarios.
And, by the way, if you really made it till the end, I am surprised and I am super grateful. Thanks for reading!