How to Detect When html2canvas Has Finished Rendering
Learn how to detect html2canvas completion with Promises, handle failures, wait for dynamic content, and export reliable screenshots.

Use the Promise returned by html2canvas(). When the Promise fulfills, its value is the rendered <canvas> element. Put your export or follow-up code after await html2canvas(element), or in .then(). Handle a rejected Promise with try/catch or .catch().
try {
const canvas = await html2canvas(document.querySelector('#invoice'));
const png = canvas.toDataURL('image/png');
download(png, 'invoice.png');
} catch (error) {
console.error('html2canvas failed:', error);
}
This Promise fulfillment is the supported completion signal in current html2canvas usage. The older onrendered callback was removed; code that still depends on it should be migrated to the Promise API. See the official getting-started documentation and changelog.
What “finished” means in html2canvas
When the Promise fulfills, html2canvas has produced a canvas for that invocation. It does not mean that every unrelated task in your application has stopped, that all network requests on the page are complete, or that the canvas is pixel-identical to the browser. html2canvas traverses the DOM, builds a representation, and paints the CSS properties it supports. Its documentation describes these rendering and cross-origin limits.

Think of completion as a boundary around one library call:
- Your code prepares the DOM and any application state.
- You call
html2canvas(element, options). - The library resolves the Promise with a canvas, or rejects it.
- Your code reads, displays, downloads, or uploads that canvas.
The onError option is different. The configuration reference describes it as a notification for a resource failure while rendering continues. It is not a “render complete” callback and should not be used to trigger export.
Use async/await for a clear completion check
Wrap the capture in an async function. The statement after await cannot run until the Promise settles successfully.
import html2canvas from 'html2canvas';
async function captureCard() {
const element = document.querySelector('.card');
if (!element) {
throw new Error('Card element was not found');
}
try {
const canvas = await html2canvas(element, {
backgroundColor: '#ffffff',
scale: window.devicePixelRatio,
useCORS: true,
imageTimeout: 15000
});
// This line runs only after html2canvas fulfilled its Promise.
document.querySelector('#preview').replaceChildren(canvas);
return canvas;
} catch (error) {
// A rejected Promise means this capture did not complete successfully.
console.error(error);
showCaptureError(error);
throw error;
}
}
captureCard();
Do not omit the await and assume a synchronous result. This is incorrect:
const canvas = html2canvas(element); // canvas is actually a Promise
console.log(canvas.toDataURL()); // TypeError
Use then() and catch() when async/await is not convenient
The Promise can be observed directly in callback-style code. The behavior is the same as await.
html2canvas(document.querySelector('#chart'), {
imageTimeout: 15000
})
.then((canvas) => {
// Fulfilled: the rendered canvas is available here.
const dataUrl = canvas.toDataURL('image/png');
document.querySelector('#output').src = dataUrl;
})
.catch((error) => {
// Rejected: report or recover from the failed capture.
console.error('Could not render chart:', error);
});
Wait for your application before calling html2canvas
html2canvas does not provide one universal event meaning “all application resources, fonts, animations, and asynchronous state are ready.” If your page renders data after an API call, changes layout after a framework update, or loads images and fonts dynamically, make those conditions explicit before starting the capture.

Wait for data and a DOM condition
function waitForSelector(selector, timeout = 10000) {
return new Promise((resolve, reject) => {
const existing = document.querySelector(selector);
if (existing) return resolve(existing);
const observer = new MutationObserver(() => {
const element = document.querySelector(selector);
if (!element) return;
observer.disconnect();
resolve(element);
});
observer.observe(document.documentElement, {
childList: true,
subtree: true
});
setTimeout(() => {
observer.disconnect();
reject(new Error(`Timed out waiting for ${selector}`));
}, timeout);
});
}
async function captureAfterData() {
await fetch('/api/report')
.then((response) => {
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
})
.then(renderReport);
const report = await waitForSelector('#report-ready');
await document.fonts.ready;
await new Promise((resolve) => requestAnimationFrame(() =>
requestAnimationFrame(resolve)
));
return html2canvas(report);
}
The two animation frames give the browser an opportunity to apply layout and paint after your state update. They are application-level preparation, not an html2canvas completion signal.
Wait for images when their timing matters
async function waitForImages(root) {
const images = [...root.querySelectorAll('img')];
await Promise.all(images.map((img) => {
if (img.complete) {
return img.decode ? img.decode().catch(() => {}) : undefined;
}
return new Promise((resolve) => {
img.addEventListener('load', resolve, { once: true });
img.addEventListener('error', resolve, { once: true });
});
}));
}
const target = document.querySelector('#profile');
await waitForImages(target);
const canvas = await html2canvas(target, {
useCORS: true,
imageTimeout: 15000
});
Even after an image loads, cross-origin rules can prevent it from being read into a canvas. Configure the image server with suitable CORS headers, use useCORS: true where appropriate, or use a permitted proxy. The browser’s same-origin policy still applies.
Options that affect completion and output
Choose options based on the capture you need. The complete list is in the configuration reference.
| Option | Purpose | Completion and reliability notes |
|---|---|---|
imageTimeout |
Maximum time to wait for images; the documented default is 15,000 ms. | A timeout does not create a separate completion event. The main Promise still settles when the capture finishes. |
onclone |
Modify the cloned document before painting. | Use it to hide controls, pause animation, or add print-only styling without changing the live page. |
onError |
Receive resource failure notifications. | Rendering can continue, so this is not a completion callback. |
useCORS, proxy, allowTaint |
Control cross-origin image handling. | Incorrect CORS settings commonly produce missing images or a canvas that cannot be exported. |
scale |
Set output pixel density; the default follows the device pixel ratio. | Higher values improve sharpness but increase memory and CPU use. |
backgroundColor |
Set the canvas background, or use null for transparency. |
Transparent output can expose differences between CSS backgrounds and the final image. |
windowWidth, windowHeight |
Set the virtual viewport used while cloning. | Useful when responsive CSS must be deterministic. |
x, y, width, height |
Capture a region or control the viewport area. | Confirm the element’s position after layout has settled. |
scrollX, scrollY |
Set scroll coordinates for fixed or sticky layouts. | Use explicit values when the current page scroll is not stable. |
foreignObjectRendering |
Ask the browser to render through SVG foreignObject where supported. | Support varies by browser and content; test the target environment. |
ignoreElements, data-html2canvas-ignore |
Exclude elements from the clone. | Remove video, blinking cursors, ads, or controls that make output nondeterministic. |
removeContainer |
Remove the temporary cloned container after rendering. | Keep the default cleanup behavior unless you have a specific debugging need. |
logging |
Enable diagnostic logging. | Turn it on while investigating missing resources or unsupported styles. |
Freeze changing content with onclone
const canvas = await html2canvas(document.querySelector('#dashboard'), {
onclone: (clonedDocument) => {
clonedDocument.querySelectorAll('.live-clock, .cursor').forEach((node) => {
node.style.visibility = 'hidden';
});
clonedDocument.querySelectorAll('video').forEach((video) => {
video.pause();
});
},
logging: true
});
Export only after the Promise fulfills
For a download, convert the returned canvas after await. For large images, prefer toBlob() over a giant base64 string.
function downloadBlob(blob, filename) {
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
link.click();
URL.revokeObjectURL(url);
}
async function savePng(element) {
const canvas = await html2canvas(element);
const blob = await new Promise((resolve, reject) => {
canvas.toBlob((value) => value ? resolve(value) : reject(new Error('toBlob failed')), 'image/png');
});
downloadBlob(blob, 'capture.png');
}
Common errors and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
canvas.toDataURL is not a function |
You used the Promise as if it were a canvas. | Add await or use .then((canvas) => ...). |
| Export code runs too early | The capture call was not awaited. | Keep all canvas consumers after Promise fulfillment. |
Old onrendered handler never runs |
The legacy callback was removed. | Replace it with await html2canvas(...). |
| Some images are missing | Cross-origin restrictions, failed URLs, or an image timeout. | Fix image URLs and CORS headers, set useCORS, configure a proxy when allowed, and review imageTimeout. |
| Canvas export throws a security error | A tainted canvas contains unreadable cross-origin pixels. | Serve assets with CORS, avoid tainting resources, or remove those assets from the capture. |
| Text or CSS differs from the page | html2canvas supports a subset of CSS and reconstructs the DOM. | Simplify unsupported styles, use onclone, or use a browser screenshot service for browser-native rendering. |
| Blank or incomplete dynamic section | Application data or layout was not ready before the call. | Wait for the data, selector, images, fonts, and a settled layout first. |
| Memory pressure or a slow capture | Large dimensions, high scale, or a complex DOM. |
Capture a smaller region, reduce scale, hide unnecessary nodes, and avoid repeated concurrent captures. |
Performance and reliability checklist
- Capture the smallest element that satisfies the requirement instead of the entire document.
- Set a deliberate
scale; device-pixel-ratio defaults can create very large canvases on high-density displays. - Disable animations and blinking UI in
oncloneso repeated captures are stable. - Wait for fonts and critical images before starting the library call.
- Use a timeout around your own preparation steps so a missing API response cannot leave a job pending forever.
- Record Promise rejection details and the browser, viewport, URL, and option set used for the failed capture.
- Do not treat a fulfilled Promise as proof of pixel-perfect browser output; compare against the library’s documented CSS and cross-origin limitations.
html2canvas runs in the user’s browser, so its cost is browser CPU, memory, and time rather than an API charge. For server-side batch work, unattended jobs, or pages you do not control, a browser-based screenshot API can remove much of that setup.
Or skip the browser setup
ScreenshotNeo provides a website screenshot API and MCP server. One GET request returns a PNG, JPEG, WebP, or PDF. It accepts cookie and consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be turned off. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing result. 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 63 options, including full-page and element capture, device presets, retina scale, custom CSS and JavaScript, clicks, waits, request blocking, headers, cookies, user agents, timezone, geolocation, transparency, resizing, caching, signed links, asynchronous jobs, webhooks, bulk capture, usage, and OpenAPI.
cURL
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://stripe.com \
-o shot.webp
Python
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
Node.js
const q = new URLSearchParams({
access_key: 'YOUR_API_KEY',
url: 'https://stripe.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());
require('fs').writeFileSync('shot.webp', image);
ScreenshotNeo has 1,000 shots per month free with no card. Paid plans start at $5 for 3,000 shots; yearly billing gives two months free, and every feature is included on every plan. Create a free ScreenshotNeo account.
FAQ
Does html2canvas return a Promise?
Yes. The Promise fulfills with the rendered canvas or rejects when that capture call fails.
Can I use onError to know when rendering ended?
No. It reports resource failures while rendering can continue. Observe the main Promise for completion.
Why is my fulfilled canvas different from the browser screenshot?
html2canvas reconstructs the DOM and supports a defined subset of CSS. Cross-origin assets and unsupported properties can change the result.
Should I wait for window.onload?
Only if your application uses it as one of its readiness conditions. It does not guarantee that framework state, fonts, animations, or later API content are finished.
How do I know whether a capture failed?
Handle Promise rejection with try/catch or .catch(), and log the error together with the URL, element, browser, and options.


