Short answer: Puppeteer removed page.waitForTimeout in v22.0.0, so on a current install the method does not exist and the call throws TypeError: page.waitForTimeout is not a function. Replace it with a one line promise: await new Promise(r => setTimeout(r, 1000)).
By the end of this you will have the one line fix, the wait strategy that actually prevents the error instead of papering over it, and the point where you should stop driving a browser at all.
Why does page.waitForTimeout throw "is not a function"?
Because the method was deleted. page.waitForTimeout was deprecated for a few releases and then removed for good in Puppeteer v22.0.0, so on any version from v22 onward the property is simply not on the page object. JavaScript reads page.waitForTimeout as undefined, you try to call undefined like a function, and Node throws TypeError: page.waitForTimeout is not a function. The error is not a bug and not a typo; it is an accurate report that the method you are calling no longer ships.
The reason it surprises people is timing. The line worked for months, then a routine npm update or a fresh npm install on a new machine pulled Puppeteer v22 or later, and the same script that ran yesterday now throws on the first wait. Nothing in your code changed. The dependency under it did.
This bites screenshot scripts hardest, because the classic pattern is await page.goto(url), then a fixed waitForTimeout to "let it settle", then await page.screenshot(). If the only reason you are running a headless browser is to capture a public page, a hosted screenshot API like ScreenshotRender skips this whole category of breakage: you send a URL, you get a PNG, and there is no Puppeteer version in your dependency tree to drift. If you need the full browser for scraping or testing, the fixes below are what you want.
What is the fix for "waitForTimeout is not a function"?
Replace the call with a plain promise that resolves after a delay: await new Promise(resolve => setTimeout(resolve, 1000)) pauses for one second using the standard setTimeout that has always been in the runtime. This is the exact replacement the Puppeteer team pointed to when they removed the method, so it is the literal one for one swap.
If you want it to read better, use the promise based timer that ships with Node. Add import { setTimeout } from 'node:timers/promises' at the top of the file and the wait becomes await setTimeout(1000). The node:timers/promises version is built for exactly this and avoids the slightly noisy new Promise wrapper. Both behave identically and neither can be removed by a Puppeteer upgrade, because neither is a Puppeteer method.
That clears the error. It does not, however, make your script correct, because a fixed sleep was probably the wrong tool to begin with.
What should you wait for instead of a fixed timeout?
Wait on a real signal that the thing you care about is ready, not on a guessed number of milliseconds. A fixed sleep is a bet: guess too low and you capture a half-rendered page, guess too high and every run wastes the difference. Waiting on the actual page state wins both ways. Here is the bad version first, then the three replacements ordered from loosest to tightest.
The bad version is any hard-coded pause: await new Promise(r => setTimeout(r, 5000)) "to be safe". It is flaky on a slow load and wasteful on a fast one. Use it only as a last resort when nothing on the page tells you it is done.
- Wait for an element.
await page.waitForSelector('.results')resolves the instant the element exists and rejects on timeout. This is what you actually meant nine times out of ten: you were waiting for content to appear, not for time to pass. - Wait for a condition.
page.waitForFunctionruns a predicate inside the page until it returns truthy:await page.waitForFunction(() => document.querySelectorAll('.card').length > 10). Reach for this when readiness is a computed fact, like a count of items or a global variable being set. - Wait for navigation or network.
page.waitForNavigationandpage.waitForNetworkIdlewait for the page to finish loading rather than for an element. Useful right after a click that triggers a redirect, or before a screenshot of a page whose content arrives over XHR.
The rule that survives every Puppeteer upgrade: wait for a thing, not for a duration. The thing is deterministic; the duration is a guess.
Skip the Chromium build, the version drift, and the wait-strategy guessing.
If you are only inside Puppeteer to grab a screenshot, one GET request to ScreenshotRender returns a hosted PNG with cookie banners and ads already stripped. A real Chromium renders the JavaScript on our infrastructure, so there is no waitForTimeout to break. 100 free screenshots per month, no credit card.
Get an API keyWhen does waiting in Puppeteer fail entirely?
Sometimes no wait strategy is enough, and the honest move is to recognize which case you are in before you spend a day on it. Three situations where fixing the wait does not fix the problem:
- The page is Cloudflare-protected. A vanilla headless Chromium gets served a challenge page, so your
waitForSelectortimes out waiting for content that never loads. The wait is correct; the browser fingerprint is the problem. You need a stealth setup and often a residential proxy, or a service that bakes that in. See our guide to screenshotting Cloudflare-protected sites for what actually changes. - You only ever wanted a screenshot. If the browser exists purely to call
page.screenshot(), you are maintaining a Chromium install, font packages, and a wait strategy to produce one image. A single GET does the same job:https://screenshotrender.com/api/v1/screenshot?apiKey=YOUR_API_KEY&url=https://news.ycombinator.com&fullPage=true. ThefullPage=trueparameter captures the whole scrollable page, and the render happens on infrastructure that stays current so there is no version to chase. - CI cost outweighs the benefit. Installing and launching Chromium on every pull request adds setup time and memory to a runner that would otherwise finish in seconds. If the browser is only there for a handful of screenshots, an HTTP call from the workflow is faster and cheaper than provisioning a browser per run.
For the first case specifically, ScreenshotRender ships Stealth Mode on the Hobby plan and above, so a Cloudflare-protected URL returns the real page from the same one line call rather than a challenge screen. The point is not that browsers are bad; it is that the version drift that caused your waitForTimeout error is one of several costs you stop paying when the render is not your job. If you are evaluating which screenshot API to use, we compared the options in our guide to the best screenshot API.
Common questions about page.waitForTimeout
Was page.waitForTimeout removed from Puppeteer?
Yes. page.waitForTimeout was removed in Puppeteer v22.0.0 after being deprecated in the releases before it. On any install from v22 onward the method does not exist, so calling it throws TypeError: page.waitForTimeout is not a function. If you need the method back temporarily you can pin Puppeteer to a v21 release, but the real fix is to replace the call rather than freeze the version.
Is waitForTimeout deprecated in Puppeteer?
It was deprecated first, then removed. The Puppeteer team marked page.waitForTimeout as deprecated with a console warning, told everyone to switch to new Promise(r => setTimeout(r, ms)), and then deleted the method in v22.0.0. So on current Puppeteer it is not deprecated, it is gone, which is why you see is not a function instead of a deprecation notice.
How do I make Puppeteer sleep for a few seconds?
Use a plain promise: await new Promise(resolve => setTimeout(resolve, 3000)) pauses for three seconds. Cleaner still, import the promise based timer with import { setTimeout } from 'node:timers/promises' and write await setTimeout(3000). Both work on every Node version that current Puppeteer supports, and neither depends on a Puppeteer method that can be removed under you.
How do I wait for a specific element in Puppeteer?
Call await page.waitForSelector('.your-element'), which resolves as soon as the element appears in the DOM and rejects on timeout. This is almost always what you wanted when you reached for a fixed sleep: you were not really waiting for time to pass, you were waiting for something on the page to be ready. Waiting for the element directly is faster on quick loads and safer on slow ones.
Does this error happen in Playwright too?
No. Playwright still ships page.waitForTimeout and it works, though the docs discourage it in test code for the same reason: a fixed sleep is flaky compared to waiting on a real signal. The is not a function error is specific to Puppeteer v22 and later, where the method was removed. If you are hitting a different Playwright failure around navigations, that is the separate execution context was destroyed error, which has its own fix.
The honest takeaway: this error is the cheapest one Puppeteer gives you, because the fix is a single line and the upgrade that caused it was a good idea. Swap the removed method for setTimeout, then go one better and wait on the element or condition you actually care about, and your script stops guessing at time.



