cyberangles blog

Why You Can’t Catch JavaScript Errors When Adding Scripts Dynamically (And How to Fix It)

In modern web development, dynamic script loading has become a staple for optimizing performance, conditionally loading features, and integrating third-party tools (e.g., analytics, ads). Instead of loading all scripts upfront in your HTML, you can inject <script> tags into the DOM programmatically using JavaScript. This approach improves page load times by deferring non-critical scripts until they’re needed.

However, there’s a hidden pitfall: errors in dynamically added scripts often slip through traditional try/catch blocks. If you’ve ever wondered why your carefully placed try/catch fails to catch errors from dynamically loaded scripts, you’re not alone. In this blog, we’ll demystify why this happens and explore actionable solutions to regain control over error handling.

2025-12

Table of Contents#

  1. What is Dynamic Script Loading?
  2. The Problem: Why Errors Slip Through try/catch
  3. Deep Dive: How Browsers Execute Dynamically Added Scripts
  4. Solutions to Catch Dynamic Script Errors
  5. Best Practices for Dynamic Script Error Handling
  6. Conclusion
  7. References

What is Dynamic Script Loading?#

Dynamic script loading is the practice of creating and injecting <script> elements into the DOM using JavaScript, rather than declaring them statically in HTML. This gives developers granular control over when and how scripts are loaded.

Common Use Cases:#

  • Lazy Loading: Load non-critical scripts (e.g., chat widgets, modals) only when a user interacts with a page.
  • Conditional Loading: Load polyfills or feature-specific scripts only if the browser lacks support (e.g., loading IntersectionObserver polyfill for older browsers).
  • Third-Party Integrations: Dynamically inject scripts for tools like Google Analytics, Stripe, or social media widgets based on user actions.

Example: Basic Dynamic Script Injection#

Here’s how you might dynamically load a script:

// Create a script element
const script = document.createElement('script');
// Set the script source
script.src = 'https://example.com/dynamic-script.js';
// Append to the DOM to trigger loading
document.body.appendChild(script);

The Problem: Why Errors Slip Through try/catch#

At first glance, you might assume wrapping the script injection in a try/catch block would catch errors in the dynamic script. Let’s test this:

Example: Failed try/catch Attempt#

try {
  const script = document.createElement('script');
  script.src = 'https://example.com/buggy-script.js'; // Contains: throw new Error('Oops!');
  document.body.appendChild(script);
} catch (error) {
  console.error('Caught error:', error); // This NEVER runs!
}

Result: The error Oops! is thrown, but the catch block is never triggered. Why?

Deep Dive: How Browsers Execute Dynamically Added Scripts#

To understand why try/catch fails, we need to explore how browsers handle dynamically added scripts:

1. Asynchronous Loading and Execution#

By default, dynamically added scripts load asynchronously. When you append the <script> element to the DOM, the browser starts fetching the script in the background. Meanwhile, the rest of your JavaScript (including the try/catch block) continues executing. By the time the script finishes loading and executes, the try/catch block has already exited.

2. Separate Execution Context#

Even if the script loads synchronously (e.g., using async: false, which is deprecated), the script executes in the global scope, not the scope of the try/catch block that injected it. Errors in the script bubble up to the global scope, bypassing the original try/catch.

3. The Event Loop#

JavaScript is single-threaded, and the event loop manages asynchronous operations. When you append the script, the browser queues the network request and script execution for later. The try/catch block runs immediately (synchronously) and exits before the script executes. Thus, the error occurs long after the try/catch has finished.

In short: try/catch only catches errors in the same synchronous execution context. Dynamically loaded scripts execute in a separate context, outside the original try/catch scope.

Solutions to Catch Dynamic Script Errors#

Now that we understand the problem, let’s explore solutions to catch errors in dynamically added scripts.

Solution 1: Use the error Event for Loading Failures#

The <script> element emits an error event when it fails to load (e.g., network error, 404). This does not catch execution errors (e.g., ReferenceError in the script), but it’s critical for handling network issues.

Example: Handling Load Errors#

const script = document.createElement('script');
script.src = 'https://example.com/missing-script.js'; // 404 error
 
// Listen for loading errors
script.addEventListener('error', (event) => {
  console.error('Script failed to load:', event);
  // Handle the error (e.g., retry, load fallback)
});
 
document.body.appendChild(script);

Key Note: The error event only triggers for loading failures, not runtime errors in the script itself.

Solution 2: Wrap Script Content in try/catch (For Same-Origin Scripts)#

If the dynamic script is hosted on your domain (same-origin), you can fetch its content, wrap it in a try/catch block, and inject the wrapped code. This ensures errors in the script are caught locally.

How It Works:#

  1. Fetch the script content as text using fetch().
  2. Wrap the content in a try/catch block.
  3. Inject the wrapped code into the DOM as an inline script.

Example: Wrapping Script Content#

// Fetch the script content
fetch('https://example.com/same-origin-script.js')
  .then((response) => {
    if (!response.ok) throw new Error('Network response failed');
    return response.text(); // Get script content as text
  })
  .then((scriptContent) => {
    // Wrap the script in try/catch
    const wrappedScript = `
      try {
        ${scriptContent} // Original script code
      } catch (error) {
        console.error('Dynamic script error:', error);
        // Handle the error (e.g., log to monitoring service)
      }
    `;
 
    // Inject the wrapped script into the DOM
    const script = document.createElement('script');
    script.textContent = wrappedScript; // Use textContent for inline code
    document.body.appendChild(script);
  })
  .catch((error) => {
    console.error('Fetch or wrapping failed:', error);
  });

Limitations:

  • Requires CORS if the script is cross-origin (most third-party scripts block this).
  • Increases latency (you must wait for the fetch to complete before executing).

Solution 3: Global window.onerror or window.addEventListener('error')#

The global error event (via window.onerror or window.addEventListener('error')) catches uncaught errors across the entire page, including those from dynamic scripts.

Example: Global Error Listener#

// Listen for global errors
window.addEventListener('error', (event) => {
  console.error('Global error caught:', {
    message: event.error.message,
    source: event.filename, // Script URL where error occurred
    line: event.lineno,
    column: event.colno
  });
  // Prevent the browser's default error logging (optional)
  event.preventDefault();
});
 
// Inject the dynamic script
const script = document.createElement('script');
script.src = 'https://example.com/buggy-script.js'; // Throws "Oops!"
document.body.appendChild(script);

Cross-Origin Scripts and "Script Error":
For cross-origin scripts (e.g., https://third-party.com/script.js), browsers restrict error details to Script error. for security reasons. To fix this:

  1. Add the crossorigin="anonymous" attribute to the script element.
  2. Ensure the third-party server includes CORS headers (e.g., Access-Control-Allow-Origin: *).

Example: Fixing Cross-Origin Error Details#

const script = document.createElement('script');
script.src = 'https://third-party.com/script.js';
script.crossOrigin = 'anonymous'; // Critical for error details
document.body.appendChild(script);

Now, window.onerror will receive full error details (message, line number, etc.).

Solution 4: eval() (Last Resort)#

eval() executes a string as code in the current scope. While generally discouraged (due to security and performance risks), it can catch errors in dynamic scripts if other methods fail.

Example: Using eval() with try/catch#

try {
  // Fetch the script content (same-origin only, due to CORS)
  const response = await fetch('https://example.com/script.js');
  const scriptContent = await response.text();
  // Evaluate the script in the current scope
  eval(scriptContent);
} catch (error) {
  console.error('Error in dynamic script:', error);
}

Why Avoid eval()?:

  • Executes code in the current scope, risking variable leaks or unintended side effects.
  • Blocks JavaScript optimizations (slower execution).
  • Security risks if the script content is untrusted (e.g., XSS attacks).

Best Practices for Dynamic Script Error Handling#

To minimize issues with dynamic script errors:

  1. Combine Loading and Execution Error Handling: Use script.addEventListener('error') for network failures and window.onerror for execution errors.
  2. Wrap Same-Origin Scripts: Use the fetch-and-wrap method for scripts you control (same-origin) to catch errors locally.
  3. Handle Cross-Origin Scripts Carefully: Use crossorigin="anonymous" and ensure third-party servers send CORS headers to get full error details in window.onerror.
  4. Avoid eval(): Only use it as a last resort for legacy or unchangeable scripts.
  5. Log Errors to Monitoring Tools: Forward caught errors to services like Sentry or Datadog for debugging in production.
  6. Test Edge Cases: Simulate network failures, 404s, and invalid script content to ensure your error handlers work.

Conclusion#

Dynamic script loading is powerful, but errors in these scripts evade traditional try/catch blocks due to asynchronous execution and separate contexts. By leveraging script error events for network issues, wrapping same-origin scripts in try/catch, and using global error listeners, you can regain control over error handling.

Remember: The key is understanding that dynamically loaded scripts execute in a separate context—so you need context-aware solutions like event listeners or wrapped execution. With these techniques, you can build robust, error-resilient applications.

References#