How To Fix JavaScript Error on a Page / JavaScript Error Handling

When I first started writing JavaScript, nothing frustrated me more than those mysterious red errors popping up in the console. Sometimes they stopped my code cold; other times they quietly broke parts of my page. Over time, I realized that the key wasn’t just fixing errors when they appeared, but building a reliable way to capture and handle them.

I’ll walk you through a complete, drop-in script that I use in both web pages and browser extensions. It catches almost everything: runtime errors, broken resources, failed promises, and even logs from console.error. It also adds helpful tools like retry-with-backoff for fragile code. Finally, I’ll share practical ways to report those errors back to yourself either to a server or to an extension background script.

Why Error Happen

JavaScript errors usually fall into three buckets:

  1. Runtime errors — things like ReferenceError: nonExistentCall is not defined.
  2. Resource errors — a missing script, stylesheet, or image.
  3. Promise rejections — failed fetch calls or async code that throws without a handler.

The problem is that once one of these fires, it can stop execution in unexpected ways. That’s why I needed a solution that:

  • Captures errors globally.
  • Keeps code running by retrying where possible.
  • Lets me safely interact with the DOM without crashing.
  • Sends me a report of what actually went wrong.

The Complete Script

Here’s the full script. You can paste this into a <script> tag on your page or bundle it inside a browser extension content script.

<script>
/** ========= CONFIG ========= **/
const ERROR_ENDPOINT = "/error-intake";         // your server endpoint (if using fetch)
const EXT_MSG_TYPE   = "CLIENT_ERROR";          // message type if using chrome.runtime.sendMessage
const MAX_BUFFER     = 20;                      // max buffered reports before flush
const MAX_RETRIES    = 5;                       // defExe max attempts
const BASE_DELAY_MS  = 250;                     // defExe base delay (exponential backoff)
const ENABLE_FETCH   = false;                   // set true if you have a server to receive errors
const ENABLE_EXTMSG  = typeof chrome !== "undefined" && chrome.runtime?.id;

/** ========= UTILITIES ========= **/
function nowISO(){ return new Date().toISOString(); }

function extraInfo() {
  return {
    url: location.href,
    userAgent: navigator.userAgent,
    platform: navigator.platform,
    language: navigator.language || navigator.userLanguage,
    cookiesEnabled: navigator.cookieEnabled,
    viewport: { w: window.innerWidth, h: window.innerHeight },
    time: nowISO()
  };
}

/** ========= TRANSPORTS ========= **/
async function mailError(payload) {
  ErrorBuffer.push(payload);
  if (ErrorBuffer.length >= MAX_BUFFER) await flushErrors();
}

async function flushErrors() {
  if (ErrorBuffer.length === 0) return;
  const batch = ErrorBuffer.splice(0, ErrorBuffer.length);
  try {
    if (ENABLE_FETCH) {
      await fetch(ERROR_ENDPOINT, {
        method: "POST",
        headers: {"Content-Type":"application/json"},
        keepalive: true,
        body: JSON.stringify({ batch })
      });
    }
    if (ENABLE_EXTMSG) {
      chrome.runtime.sendMessage({ type: EXT_MSG_TYPE, batch });
    }
  } catch (e) {
    ErrorBuffer.unshift(...batch);
  }
}

const ErrorBuffer = [];
for (const ev of ["visibilitychange","pagehide","beforeunload"]) {
  window.addEventListener(ev, () => { if (document.visibilityState !== "visible") flushErrors(); });
}

/** ========= DOM READINESS ========= **/
function onReady(cb){
  if (document.readyState === "complete" || document.readyState === "interactive") {
    queueMicrotask(cb);
  } else {
    document.addEventListener("DOMContentLoaded", cb, { once:true });
  }
}

/** ========= SAFE DOM HELPERS ========= **/
function showIfExists(id){
  const el = document.getElementById(id);
  if (el) {
    el.style.display = "block";
    return true;
  } else {
    mailError({
      type: "DOM_MISS",
      message: `Element #${id} not found`,
      meta: extraInfo()
    });
    return false;
  }
}

/** ========= CONSOLE WRAPPER ========= **/
const Xe = {
  error(msg, meta={}) {
    console.error(msg);
    mailError({ type:"CONSOLE_ERROR", message: String(msg), meta: {...extraInfo(), ...meta} });
  },
  warn(msg, meta={}) {
    console.warn(msg);
    mailError({ type:"CONSOLE_WARN", message: String(msg), meta: {...extraInfo(), ...meta} });
  },
  log(msg, meta={}) {
    console.log(msg);
    mailError({ type:"CONSOLE_LOG", message: String(msg), meta: {...extraInfo(), ...meta} });
  }
};

/** ========= GLOBAL ERROR CAPTURE ========= **/
window.onerror = function(message, source, lineno, colno, error){
  mailError({
    type: "ONERROR",
    message: String(message),
    source, lineno, colno,
    stack: error && error.stack,
    meta: extraInfo()
  });
  return true;
};

window.addEventListener("error", function(ev){
  const t = ev.target;
  if (t && t !== window && (t.src || t.href)) {
    mailError({
      type: "RESOURCE_ERROR",
      message: "Resource failed to load",
      tag: t.tagName,
      url: t.src || t.href,
      meta: extraInfo()
    });
  }
}, true);

window.addEventListener("unhandledrejection", function(ev){
  const reason = ev.reason || {};
  mailError({
    type: "UNHANDLED_REJECTION",
    message: String(reason.message || reason),
    stack: reason.stack,
    meta: extraInfo()
  });
});

/** ========= “DEFINITELY EXECUTE” with BACKOFF ========= **/
function defExe(fn, opts = {}) {
  const { max = MAX_RETRIES, baseDelay = BASE_DELAY_MS } = opts;
  let attempt = 0;
  const run = () => {
    try {
      fn();
    } catch (e) {
      attempt += 1;
      mailError({
        type: "DEFEXE_CATCH",
        message: `Attempt ${attempt}/${max} failed: ${e && e.message}`,
        stack: e && e.stack,
        fnName: fn.name || "anonymous",
        meta: extraInfo()
      });
      if (attempt < max) {
        const delay = baseDelay * Math.pow(2, attempt - 1);
        setTimeout(run, delay);
      } else {
        Xe.error(`defExe giving up after ${max} attempts for ${fn.name || "anonymous"}`);
      }
    }
  };
  run();
}

/** ========= EXAMPLE USAGE ========= **/
onReady(() => {
  showIfExists("iNeedThisElement");
  function initWidgets(){
    const el = document.querySelector("[data-widget]");
    if (!el) throw new Error("widget root missing");
    el.textContent = "Widget initialized at " + nowISO();
  }
  defExe(initWidgets);
  Xe.warn("Demo warning");
  Xe.log("Demo log");
});
</script>

How It Works

  • window.onerror catches runtime errors.
  • Resource error listener captures missing images, scripts, or styles.
  • unhandledrejection ensures async Promise failures are reported.
  • Xe wrapper extends console.log, warn, and error to include extra metadata.
  • defExe retries fragile code with exponential backoff until it works or fails out.
  • onReady guarantees safe DOM access without guessing page state.
  • Reporting happens through either a fetch call to your backend or chrome.runtime.sendMessage inside an extension.

Extra Tricks I Use

  • Sampling — only send 20% of logs if a page is too noisy.
  • De-duplication — prevent flooding with the same error by ignoring repeats within 10 seconds.
  • Element hints — attach a snippet of failing HTML to resource error logs.
  • Custom retry policies — tweak max attempts or base delay per function.

Final Thought

JavaScript errors will always happen it’s the nature of dynamic code, unpredictable networks, and fragile DOM states. What matters is how we capture, report, and recover from them. This script has saved me countless hours of head-scratching. Instead of wondering why a widget silently broke, I get a clear log with stack traces, environment info, and retry attempts. Whether you’re building a simple website or a complex browser extension, a robust error handling layer like this makes your code far more resilient.

Related blog posts