ScreenshotNeo

BlogHow-to

How to Navigate Back and Forward with Puppeteer

Use Puppeteer’s goBack() and goForward() methods to traverse browser history, wait for navigation safely, and handle missing entries and single-page apps.

By the ScreenshotNeo team4 October 20267 min read

Use page.goBack() to visit the previous history entry and page.goForward() to visit the next one. Both return a promise that resolves to the main resource response, or null for a same-page navigation. If there is no history entry in the requested direction, the call throws. You can configure how Puppeteer waits for the navigation to finish.

This guide uses Puppeteer’s Page API. Check the documentation for the version installed in your project, since API details can vary between versions. The current reference in the research dossier identifies version 25.12.0.

Basic back and forward navigation

Call the method on the page whose active tab history you want to traverse:

const backResponse = await page.goBack();
const forwardResponse = await page.goForward();

Usually you will call one direction or the other, depending on the state of the test or automation. The following runnable example launches Chromium, opens two pages, navigates back, then forward, and closes the browser even if an operation fails:

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({ headless: true });
  try {
    const page = await browser.newPage();

    await page.goto('https://example.com');
    await page.goto('https://example.org');

    const backResponse = await page.goBack();
    console.log('After back:', page.url());
    console.log('Back response:', backResponse ? backResponse.status() : null);

    const forwardResponse = await page.goForward();
    console.log('After forward:', page.url());
    console.log('Forward response:', forwardResponse ? forwardResponse.status() : null);
  } finally {
    await browser.close();
  }
})();

For an ES module, replace const puppeteer = require('puppeteer') with import puppeteer from 'puppeteer'. The browser binary must be available to your Puppeteer installation or configured for your environment.

Wait for the navigation you need

goBack() and goForward() accept optional WaitForOptions. The documented defaults are a 30-second timeout and waitUntil: 'load'. You can provide another lifecycle condition, a timeout, or an abort signal.

const response = await page.goBack({
  waitUntil: 'domcontentloaded',
  timeout: 15000,
});

Supported lifecycle conditions include load, domcontentloaded, networkidle0, and networkidle2. You can give one condition or an array; with an array, the wait succeeds after all specified events have fired. Choose a condition that matches what the next step needs. For example, domcontentloaded can be enough to inspect the initial document, while an application that populates content after load may need an explicit selector wait.

await page.goForward({
  waitUntil: ['domcontentloaded', 'networkidle2'],
  timeout: 20000,
});

Set a page-wide default navigation timeout when the same limit should apply to multiple navigation calls:

page.setDefaultNavigationTimeout(20000);
await page.goBack();

The per-call timeout option lets a particular traversal use a different limit. For cancellation, pass an AbortSignal as signal:

const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 5000);

try {
  await page.goBack({ signal: controller.signal });
} finally {
  clearTimeout(timer);
}

See Puppeteer’s Page.goBack reference and WaitForOptions reference for the API details relevant to your installed version.

Coordinate a history action with a navigation wait

When you need to observe a navigation caused by an action, arrange the wait before starting that action. Puppeteer documents a race condition when code begins waiting only after the action has already triggered navigation. Use Promise.all() to start both together:

const [response] = await Promise.all([
  page.waitForNavigation({ waitUntil: 'domcontentloaded' }),
  page.goBack({ waitUntil: 'domcontentloaded' }),
]);

console.log('Response:', response ? response.status() : null);

In most cases, awaiting page.goBack() or page.goForward() directly is enough because each method already waits for navigation according to its options. Use the combined pattern when you specifically need a separate navigation waiter or are coordinating a different action that triggers the history change. Do not wait for navigation only after a click or other trigger has completed.

Understand responses, same-page changes, and missing entries

  • Main resource response: A document navigation typically resolves with the response for the main resource. Inspect it for status or headers when needed.
  • Same-page navigation: A history traversal that changes the URL without fetching a new document can resolve to null. Check for null before reading response properties.
  • No entry in that direction: If there is no previous or next history entry, the method throws. Handle this if the direction depends on the page’s earlier interactions.
async function tryHistoryMove(page, direction) {
  try {
    const response = direction === 'back'
      ? await page.goBack({ waitUntil: 'domcontentloaded' })
      : await page.goForward({ waitUntil: 'domcontentloaded' });

    return {
      moved: true,
      url: page.url(),
      status: response ? response.status() : null,
      samePageOrNoDocumentResponse: response === null,
    };
  } catch (error) {
    return {
      moved: false,
      url: page.url(),
      error: error.message,
    };
  }
}

The null response is not the same as a failed navigation. It indicates that the traversal did not produce a main resource response, as can happen for a same-page transition. A thrown error means the requested traversal or its wait failed; inspect the error and current URL to determine which case occurred.

History navigation in single-page applications

Puppeteer counts a URL change made through the History API as navigation, even when the page does not fetch a new document. This is common in single-page applications: the address can change and the application can render a new view without a traditional document request.

After a same-document history change, wait for the application state you need rather than assuming a new document load proves the view is ready:

await page.goBack({ waitUntil: 'domcontentloaded' });
await page.waitForSelector('[data-page="previous-view"]');

Use a selector that the application actually renders for the destination state. If the history action can either change the document or update the current document, keep handling a null response.

Common problems and fixes

Symptom Likely cause Fix
The call throws because no history entry exists The page has not navigated in that direction, or the current position is already at the beginning or end of its history. Only traverse after the flow has created the expected entry. Catch the error when history availability depends on the user journey.
The navigation wait times out The destination did not reach the selected lifecycle event before the timeout, or the page is still loading resources. Choose a suitable waitUntil, increase the call timeout or page default, and wait for a destination-specific selector when application readiness matters.
Code fails when reading response.status() The result was null, which can happen for same-page navigation. Check that the response exists before reading its status or other response fields.
The address changes but the expected content is absent A single-page application updated history without a new document, and its view has not finished rendering. Wait for an element or state that identifies the destination view.
The navigation wait seems to miss a transition The wait was started after the action had already triggered navigation. Start the wait and triggering action concurrently with Promise.all().
Behavior differs from an example The installed Puppeteer version may differ from the version represented by the current API reference. Consult the API reference matching the version in the project lockfile.

Performance, reliability, and cost

History traversal avoids writing your own URL navigation logic, but a call can still wait on the destination’s lifecycle condition. A broad condition such as network idle may take longer on pages with ongoing requests; use the narrowest completion condition that is safe for the next step, then wait for an application selector if needed.

For reliable automation, make the prior navigation and expected history position explicit, configure a timeout that fits the task, and handle both a null response and a thrown error. Browser automation also requires managing the browser process: close it in a finally block so failures do not leave it running. The cited API behavior does not establish a compatibility matrix across all browsers, versions, or history-entry types, so verify against the Puppeteer version and browser used by your project.

goBack() and goForward() are Puppeteer methods; there is no per-call ScreenshotNeo charge associated with them. If your broader task is to capture a page as an image or PDF, ScreenshotNeo offers a separate screenshot API with usage-based plans described on its site.

Or skip the browser setup

If you need a screenshot instead of controlling browser history yourself, ScreenshotNeo returns a PNG, JPEG, WebP, or PDF from one GET request. Its options include full-page capture, element selection, viewport and device settings, custom waits, and custom CSS or JavaScript. See the ScreenshotNeo API documentation for parameters and setup.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

ScreenshotNeo accepts cookie and consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets before the shot; each step can be turned off. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers report the page verdict and billing status. Its MCP server provides take_screenshot, get_page_info, and capture_pdf for AI agents. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.

FAQ

How do I go back one page in Puppeteer?

Await page.goBack() on the page instance you want to control.

How do I move forward in Puppeteer?

Await page.goForward(); it moves to the next entry when one exists.

Does a History API URL change count as navigation?

Yes. Puppeteer includes History API URL changes in its definition of navigation, even when no new document is fetched.