Advertisement
🦉 Advanced OWL

OWL JS Error Handling – Boundaries, Async Errors & Recovery

Unhandled errors in OWL components crash the component that threw and propagate up the tree. Without an error boundary, a bug in one child widget can bring down the entire page. OWL provides two complementary error-handling mechanisms: onError for parent components to catch errors from their children and show fallback UI, and try/catch inside lifecycle hooks for handling expected async errors. This lesson covers both patterns, plus async error pitfalls, error recovery, and production logging strategies.

⏱️ 18 min read 🎯 Intermediate 📅 Updated 2026 👁️ Lesson 5 of 6

onError — Error Boundary Components

onError registers a catch handler on a component. Any error thrown by a descendant component's render, lifecycle hook, or async operation is caught here. The catching component can then show fallback UI instead of crashing.

JavaScript – error boundary component
import { Component, xml, useState, onError } from "@odoo/owl";

class ErrorBoundary extends Component {
  static template = xml`
    <t t-if="state.error">
      <div class="error-boundary">
        <h3>⚠️ Something went wrong</h3>
        <p t-esc="state.errorMessage"/>
        <details t-if="state.errorStack">
          <summary>Technical details</summary>
          <pre t-esc="state.errorStack"/>
        </details>
        <button t-on-click="retry">Try Again</button>
      </div>
    </t>
    <t t-else="">
      <t t-slot="default"/>
    </t>
  `;

  state = useState({ error: false, errorMessage: "", errorStack: "" });

  setup() {
    onError((error) => {
      console.error("[ErrorBoundary] Caught:", error);

      this.state.error        = true;
      this.state.errorMessage = error?.message || "An unexpected error occurred.";
      this.state.errorStack   = error?.stack    || "";

      // Optionally: report to error tracking service
      // logToSentry(error);
    });
  }

  retry() {
    // Reset the error state to attempt re-rendering the children
    this.state.error        = false;
    this.state.errorMessage = "";
    this.state.errorStack   = "";
    // Note: the child component needs a fresh key to truly remount
  }
}

// Usage — wrap risky widgets
class Dashboard extends Component {
  static components = { ErrorBoundary, SalesSummary, RevenueChart };

  static template = xml`
    <div class="dashboard">
      <ErrorBoundary>
        <SalesSummary/>
      </ErrorBoundary>
      <ErrorBoundary>
        <RevenueChart/>
      </ErrorBoundary>
    </div>
  `;
}
💡
Wrap each independent widget in its own error boundary

Wrapping the entire page in one error boundary means any error kills the whole page. Wrapping each independent widget in its own boundary keeps failures isolated — one broken chart does not crash the sales summary next to it. This is the Odoo pattern: each view section has independent error recovery.

try/catch in Lifecycle Hooks

For expected errors — network failures, validation errors, 404s — handle them locally with try/catch in the relevant hook:

JavaScript – try/catch in onWillStart
class PartnerWidget extends Component {
  state = useState({
    partner:   null,
    isLoading: true,
    error:     null,
  });

  setup() {
    this.orm = useService("orm");

    onWillStart(async () => {
      try {
        const [partner] = await this.orm.read(
          "res.partner",
          [this.props.partnerId],
          ["name", "email", "phone", "image_128"]
        );
        this.state.partner   = partner;
      } catch (err) {
        // Handle locally — no need for an error boundary
        this.state.error = err.message || "Failed to load partner.";
      } finally {
        this.state.isLoading = false;
      }
    });
  }

  static template = xml`
    <div>
      <div t-if="state.isLoading">Loading…</div>
      <div t-elif="state.error" class="alert-danger" t-esc="state.error"/>
      <div t-else="">
        <strong t-esc="state.partner.name"/>
        <span t-esc="state.partner.email"/>
      </div>
    </div>
  `;
}
Advertisement

Async Error Patterns

Errors in Event Handlers

JavaScript – try/catch in event handlers
class SaveButton extends Component {
  state = useState({ isSaving: false, error: null });

  static template = xml`
    <div>
      <button t-att-disabled="state.isSaving" t-on-click="save">
        <t t-if="state.isSaving">Saving…</t>
        <t t-else="">Save</t>
      </button>
      <p t-if="state.error" class="text-danger" t-esc="state.error"/>
    </div>
  `;

  async save() {
    this.state.isSaving = true;
    this.state.error    = null;
    try {
      await this.props.onSave();
      this.notification.add("Saved!", { type: "success" });
    } catch (err) {
      // UserError from Odoo server has a human-readable message
      this.state.error = err?.data?.message || err.message || "Save failed.";
    } finally {
      this.state.isSaving = false;
    }
  }
}

Retryable Errors with Exponential Backoff

JavaScript – retry with backoff
async function withRetry(fn, { retries = 3, delay = 500 } = {}) {
  for (let attempt = 0; attempt < retries; attempt++) {
    try {
      return await fn();
    } catch (err) {
      if (attempt === retries - 1) throw err;       // last attempt — rethrow
      await new Promise(r => setTimeout(r, delay * (attempt + 1)));  // backoff
    }
  }
}

// Usage in onWillStart:
onWillStart(async () => {
  try {
    this.state.data = await withRetry(
      () => this.orm.searchRead("sale.order", [], ["name"], { limit: 10 }),
      { retries: 3, delay: 300 }
    );
  } catch (err) {
    this.state.error = "Could not load after 3 attempts. Please refresh.";
  }
});

When to Use onError vs try/catch

ScenarioUseWhy
Expected network / validation errorstry/catchLocal handling — component knows how to recover
Third-party widget might crashonError boundaryIsolate crash, show fallback, don't know child internals
Async error in your own componenttry/catch in the async functionYou control the code — handle it explicitly
Rendering error (template bugs, null dereference)onError in parentCannot try/catch synchronous render errors
Plugin / dynamic widget systemonError boundaryUnknown plugin code should not crash the host

Error Boundaries: Stop One Broken Component Killing the App

By default, an error thrown while rendering a component can crash the whole UI. OWL's onError hook creates an error boundary — a component that catches errors from its children and shows a fallback instead of a blank page.

import { Component, useState, onError, xml } from "@odoo/owl";

class ErrorBoundary extends Component {
    static template = xml`
        <t t-if="state.failed">Something went wrong.</t>
        <t t-else=""><t t-slot="default"/></t>`;   // render children
    setup() {
        this.state = useState({ failed: false });
        onError((error) => {          // catches errors from child components
            this.state.failed = true;
            console.error(error);     // log for debugging
        });
    }
}

Wrap risky parts of the tree in the boundary; the rest of the app keeps working:

xml`<ErrorBoundary><RiskyChart/></ErrorBoundary>`;

What to know: onError catches errors from rendering and lifecycle hooks of descendant components — not from event handlers or async code you fire yourself (wrap those in your own try/catch). Place boundaries around independent sections (a dashboard widget, a sidebar) so one failing piece degrades gracefully instead of taking down everything. Always log the caught error — a silent fallback hides real bugs.

🏋️ Practical Exercise

Build a robust RemoteWidget component with full error handling:

  1. Fetch data in onWillStart with try/catch/finally. Show loading → data → error states.
  2. Add a "Refresh" button that re-fetches (simulating a network error 50% of the time with Math.random() < 0.5).
  3. Implement withRetry on the fetch — 3 attempts with 300ms backoff.
  4. Wrap RemoteWidget in an ErrorBoundary that uses t-key="state.retryKey" on the inner child, incremented on retry — this forces a full remount of the widget.
  5. Log errors to console with timestamp and component name.

🔥 Challenge Exercise

Build an error-boundary component using onError that catches errors from children and renders a fallback UI instead of crashing the app. Explain how OWL propagates errors and where to place a boundary.

📋 Summary

  • onError(callback) makes a component an error boundary — it catches errors from all descendants including render errors, lifecycle errors, and async rejections.
  • Show fallback UI in the error state and log the error for debugging. Provide a "Retry" button when recovery is possible.
  • Wrap each independent widget section in its own error boundary to isolate failures.
  • Use try/catch in lifecycle hooks and event handlers for expected errors you know how to recover from.
  • Always guard async operations with try/catch and set a loading/error state in finally.
  • Check for err?.data?.message in Odoo — server-side errors include a human-readable message in data.message.

Interview Questions

  • How do you handle errors in OWL?
  • What is the onError hook?
  • What is an error boundary?
  • What happens to an unhandled component error?
  • Where should you place error boundaries?

FAQ

Does onError catch errors in event handlers? +

Yes, if the event handler throws synchronously. For async handlers, only rejected Promises that OWL is aware of are caught. If an async event handler throws after the current render cycle, the rejection may be unhandled. Always use try/catch in async event handlers and set error state explicitly rather than relying on onError to catch async rejections.

How do I force a child component to remount after an error? +

Change the child's t-key value. OWL treats a different t-key as a completely new component instance — it unmounts the old one and mounts a fresh one. In the error boundary's retry method, increment a retryKey state value: this.state.retryKey++. The template uses <RiskyChild t-key="state.retryKey"/>. The new instance runs onWillStart fresh.