← Blog

How to Debug Paste Events in JavaScript

Paste bugs are maddening. The handler works in the test harness, works in your own browser, works in Chrome on your laptop — and then a user reports that pasting a line from Google Sheets into the same feature produces a blank cell, or a broken table, or a wall of unstyled HTML. This article walks through the workflow we use to diagnose paste problems end to end, from the first bug report to a verified fix.

1. Reproduce the paste the user actually made

The single most important step is the one developers most often skip. The way you paste into your own app is almost never the way the reporting user pasted — you copy from a test file, they copied from an email. Until you can reproduce the bug with the same source content, you are debugging a fiction.

Ask the reporter three things:

  • What application did you copy from? (Word, Chrome, Gmail, Sheets, Slack, a terminal…)
  • Can you paste that same content into a text file or email draft and send it to us? (Do not ask them to retype it.)
  • On what operating system and browser did the bug happen?

Once you have the copied content, paste it into Clipboard Inspector to see exactly which MIME types the source app produced. This is the ground truth — every subsequent debugging step refers back to it.

2. Inspect clipboardData inside your own handler

The next step is to see what your own code is receiving. The minimal instrumentation:

document.addEventListener('paste', (e) => {
  console.group('paste');
  console.log('types:', e.clipboardData.types);
  for (const t of e.clipboardData.types) {
    console.log(t, '→', e.clipboardData.getData(t));
  }
  console.log('files:', [...e.clipboardData.files]);
  console.groupEnd();
});

Drop this into the page where the bug reproduces and paste the user's content. You will see exactly what keys are present and what values they hold. Compare this against the Clipboard Inspector output: if types are missing or values differ, something in your page (often a content-editable or framework handler) is filtering the event before it reaches your code.

3. Check event ordering

A paste event fires on the deepest target first, then bubbles up. Frameworks, rich-text editors, and third-party scripts often register their own paste handlers earlier in the capture phase and call event.preventDefault() to take over the behaviour. If your handler never fires — or fires with empty clipboardData — another handler got there first.

To diagnose, register a capture-phase listener at the window level:

window.addEventListener('paste', fn, { capture: true })

Log whether any ancestor element is a content-editable or form input (input, textarea). If so, the browser's default editing logic is likely consuming the event and generating a synthetic DOM mutation in its place.

4. Test every major source application

Clipboard behaviour varies dramatically by source. A paste handler that works perfectly for text copied from a <textarea> will break when the same logical content is copied from Google Docs because Docs adds application/x-vnd.google-docs-* MIME entries, prefers its own HTML serialisation, and sometimes strips text/plain when certain rich elements are copied.

Build a regression checklist containing at least:

  • Plain text copied from another <input> in the same app.
  • Formatted text copied from a Word document.
  • Formatted text copied from Google Docs (desktop web).
  • A URL copied from the browser's address bar.
  • A URL copied from a rendered link in a web page.
  • A screenshot from Snipping Tool / macOS Shift+Cmd+4.
  • An image copied from a web page's <img> element.
  • A file copied from Finder / Explorer.
  • A selection copied from Excel or Google Sheets (single cell and a 2×2 range).
  • Content copied from a terminal emulator.

Run every source through your handler before calling a paste feature "done". The checklist takes ten minutes and saves hours of post-launch firefighting.

5. Watch for the "one source, many encodings" trap

Some applications produce different MIME types depending on what was copied within the app. Google Docs is the most notorious: copying a plain paragraph produces text/html with a specific wrapper, while copying across a page break produces an entirely different HTML structure that omits the wrapper. Outlook produces nested table HTML when you copy a meeting-invite block, but simple inline HTML when you copy a single sentence from an email body.

If a single source application produces intermittent bugs, do not assume your handler is flaky — assume the source is producing multiple shapes of input. Inspect each variant separately with Clipboard Inspector.

6. Async paste handlers

A common failure mode: the paste handler reads a file from event.clipboardData.files synchronously, then awaits an upload. By the time the upload resolves, the event object has been recycled and the file reference is invalid in some browsers. Always read the file into a Blob (or an ArrayBuffer) inside the synchronous portion of the handler, then upload from the blob:

const file = e.clipboardData.files[0];
const bytes = await file.arrayBuffer();
// bytes is now safe to use even after the event has bubbled up

If you need to read large files, consider calling event.preventDefault() early to stop the browser's default paste behaviour, then show a progress indicator while you read.

7. Differentiate permission errors from empty clipboards

The async Clipboard API (navigator.clipboard.read()) requires user permission and a user-initiated gesture. A common bug is to catch the rejected promise and treat it as "clipboard is empty", showing a generic error. In reality the clipboard is full — the browser is just refusing to hand it over because the call was not initiated from a click or keypress, or because the document lacks focus.

Log the rejection's name property. NotAllowedError means a permission problem; DataError means the clipboard really is empty of formats your page can read. Surface those as distinct UX states.

8. Reproduce on the reporter's platform

Clipboard plumbing lives deep in the operating system, and paste bugs often depend on OS and browser combinations. A bug that reproduces on Firefox on Windows may not appear on Chrome on macOS, because the two browsers read different sets of OS-level clipboard formats.

Keep a matrix of the combinations you support. When a bug is reported, reproduce on the reporter's combination first — then try adjacent combinations (same browser, different OS) to localise the fault. Cloud device labs (BrowserStack, Sauce, LambdaTest) are worth the monthly fee for this alone.

9. Ship a diagnostic build

For hard-to-reproduce bugs, add a hidden diagnostic panel behind a URL parameter or keyboard shortcut that dumps clipboardData.types and raw getData output into a copyable text block. Asking a user to press Ctrl+Shift+D after a failed paste and send you the resulting JSON is far more productive than asking them to "open DevTools and paste screenshots of the Console".

10. Write a regression test

Once you have a fix, lock the behaviour in with an automated test. Playwright and Cypress both let you programmatically set clipboard contents and fire paste events on the page:

await page.evaluate(() => navigator.clipboard.writeText(payload));
await page.keyboard.press('Control+V');

Seed the clipboard with the exact payload the reporter sent, fire paste, and assert on the resulting DOM state. Your next regression catches this exact bug automatically.

Wrapping up

Paste bugs feel chaotic because clipboard state is shared between your page and every other application on the user's computer. The antidote is to separate what the clipboard actually contains from what your handler receives from what your handler does with it — and to instrument each of those three layers. When you know the ground truth at every step, the bug has nowhere to hide.

The MIME types guide covers the "what the clipboard contains" half in depth, and the Clipboard API comparison explains when to pick one handler shape over another.