Why this chapter? Every OWL component you'll ever write is a
class, every prop and ORM result you'll handle benefits from destructuring, and every server call is asynchronous — this is the chapter where those four pieces stop being abstract syntax and start being how you'll write code daily. What is Odoo trying to solve with this? OWL's component model (class ... extends Component), its module system (/** @odoo-module **/andimport/export), and its services (orm,notification, etc.) are all built on these four JavaScript features — there's no way to write an OWL component without them. Real-world application: Theasync/awaitandtry/catchpatterns here are exactly what you'll use every time a component talks to the Odoo server — get this wrong and you get silent failures or "unhandled promise rejection" errors in production.
In the last chapter, we covered the basic building blocks of modern JavaScript. Now, we'll explore four more advanced concepts that are not just important—they are the very structure upon which OWL is built. Understanding classes, destructuring, modules, and asynchronous code is the final step before you can truly understand how an OWL component works.
The class Keyword: A Blueprint for Components
An OWL component is, at its core, a JavaScript class. A class is a blueprint for creating objects. It bundles data (properties) and the functions that operate on that data (methods) into a single, reusable package.
Think of a class as the architectural plan for a house. You can use the same plan to build many identical houses. Each house you build is an "instance" of the class.
Let's define a simple Dog class:
class Dog {
// The constructor is a special method that runs when a new instance is created.
// It's used to set up initial properties.
constructor(name, breed) {
this.name = name;
this.breed = breed;
this.energy = 100;
}
// A method is a function that belongs to the class.
bark() {
console.log(`${this.name} says: Woof!`);
}
play() {
this.energy -= 10;
console.log(`${this.name} is playing. Energy is now ${this.energy}.`);
}
// Methods can return information about the instance
getInfo() {
return `${this.name} is a ${this.breed} with ${this.energy} energy.`;
}
}
// Now, let's create two instances (two different dogs) from our blueprint.
const dog1 = new Dog("Rex", "German Shepherd");
const dog2 = new Dog("Buddy", "Golden Retriever");
dog1.bark(); // Outputs: Rex says: Woof!
dog2.play(); // Outputs: Buddy is playing. Energy is now 90.
console.log(dog1.getInfo()); // Outputs: Rex is a German Shepherd with 100 energy.
Extending a Class
The real power comes from the extends keyword. It allows you to create a new class that inherits all the properties and methods of an existing one. This is the direct prerequisite for understanding OWL components, which always start with class MyComponent extends Component.
Let's create a specialized Puppy class that inherits from Dog:
class Puppy extends Dog {
constructor(name, breed) {
// Call the parent constructor with super()
super(name, breed);
// Puppies start with less energy
this.energy = 50;
this.isTeething = true;
}
// We can add a new method that only puppies have.
chewShoe() {
if (this.isTeething) {
console.log(`${this.name} is chewing on a shoe! Bad puppy!`);
} else {
console.log(`${this.name} is too old to chew shoes.`);
}
}
// We can also override an existing method.
bark() {
console.log(`${this.name} says: Yip! Yip!`); // A puppy's bark is different.
}
// We can extend existing methods while still using the parent's functionality
play() {
super.play(); // Call the parent's play method
if (this.energy < 20) {
console.log(`${this.name} is getting tired and might nap soon.`);
}
}
}
const myPuppy = new Puppy("Leo", "Labrador");
myPuppy.play(); // Inherited from Dog, but with puppy-specific behavior
myPuppy.bark(); // Overridden. Outputs: Leo says: Yip! Yip!
myPuppy.chewShoe(); // New method. Outputs: Leo is chewing on a shoe! Bad puppy!
When you write class MyComponent extends Component, you are creating a new class that inherits all the powerful features of OWL's base Component class, just like our puppy inherited from Dog.
Destructuring Assignment: Extracting Data with Style
Destructuring is a powerful feature that allows you to extract values from arrays and objects into separate variables. It's one of the most elegant features of modern JavaScript and you'll see it everywhere in OWL code.
Object Destructuring: Unpacking Properties
Instead of accessing object properties the old way, destructuring lets you extract multiple values in one clean statement:
// The old, repetitive way
const contact = { name: "John Smith", email: "[email protected]", age: 35, department: "Sales" };
const name = contact.name;
const email = contact.email;
const age = contact.age;
// The modern, clean way with destructuring
const contact = { name: "John Smith", email: "[email protected]", age: 35, department: "Sales" };
// Extract multiple properties in one line
const { name, email, age } = contact;
console.log(name); // "John Smith"
console.log(email); // "[email protected]"
console.log(age); // 35
Advanced Object Destructuring Patterns
Renaming variables during destructuring:
const contact = { name: "John Smith", email: "[email protected]" };
// Rename variables during destructuring
const { name: fullName, email: emailAddress } = contact;
console.log(fullName); // "John Smith"
console.log(emailAddress); // "[email protected]"
Setting default values:
const contact = { name: "John Smith" }; // No email property
// Provide defaults for missing properties
const { name, email = "[email protected]", age = 0 } = contact;
console.log(email); // "[email protected]"
console.log(age); // 0
Nested destructuring:
const customer = {
name: "Acme Corp",
address: {
street: "123 Business Ave",
city: "Commerce City",
country: "USA"
},
contact: {
phone: "555-0123",
email: "[email protected]"
}
};
// Extract nested properties directly
const {
name,
address: { city, country },
contact: { email }
} = customer;
console.log(`${name} is located in ${city}, ${country}`);
console.log(`Contact them at ${email}`);
Array Destructuring: Extracting by Position
Array destructuring extracts values based on their position in the array:
const colors = ["red", "green", "blue", "yellow"];
// Extract first few elements
const [primary, secondary, tertiary] = colors;
console.log(primary); // "red"
console.log(secondary); // "green"
console.log(tertiary); // "blue"
// Skip elements you don't need
const [first, , third] = colors; // Notice the empty space to skip "green"
console.log(first); // "red"
console.log(third); // "blue"
The Rest Pattern: Gathering What's Left
The ... syntax you'll see inside destructuring is called the rest pattern. It collects whatever properties (or array items) weren't already pulled out into their own variable, bundling them into a new object (or array):
const customer = { id: 1, name: "Acme Corp", email: "[email protected]", isActive: true };
// Pull out "id" by itself, and gather everything else into "rest"
const { id, ...rest } = customer;
console.log(id); // 1
console.log(rest); // { name: "Acme Corp", email: "[email protected]", isActive: true }
This looks identical to the spread operator from Chapter 3 ({ ...customer, summary }), but the direction is reversed: spread expands an object's properties into a new one, while rest collects remaining properties into a new one. Which behavior you get depends only on where the ... appears—on the left side of = (a pattern being destructured) it's rest; on the right side (building a new value) it's spread.
Destructuring in Function Parameters
This is where destructuring becomes incredibly powerful for OWL development:
// Instead of accessing properties inside the function
const createUserCard = (user) => {
const name = user.name;
const email = user.email;
const isActive = user.isActive;
return `<div class="user-card ${isActive ? 'active' : 'inactive'}">
<h3>${name}</h3>
<p>${email}</p>
</div>`;
};
// Destructure directly in the function parameters
const createUserCard = ({ name, email, isActive = true }) => {
return `<div class="user-card ${isActive ? 'active' : 'inactive'}">
<h3>${name}</h3>
<p>${email}</p>
</div>`;
};
// Usage
const userData = { name: "Alice Johnson", email: "[email protected]", isActive: true };
console.log(createUserCard(userData));
Why Destructuring is Perfect for OWL
Destructuring is especially common in OWL for several scenarios:
1. Extracting props in components:
// In an OWL component
setup() {
const { recordId, model, readonly = false } = this.props;
// Now you can use recordId, model, and readonly directly
}
2. Working with service responses:
// orm.read returns an array — grab the first record with array destructuring
const [partner] = await this.orm.read("res.partner", [partnerId], ["name", "email"]);
const { name, email } = partner;
3. Event handling:
// Extracting data from events
onButtonClick({ target, currentTarget }) {
// Work directly with target and currentTarget
}
4. State management:
// Extracting state values
const { isLoading, errorMessage, data = [] } = this.state;
Modules: import and export
As your application grows, you can't keep all your code in one giant file. Modules allow you to split your code into separate, reusable files. You can export variables, functions, or classes from one file and import them into another where they are needed.
This is how the Odoo asset system works. Each JavaScript file is a module.
Basic Export and Import
Let's imagine we have a file called utils.js with some helper functions:
// File: utils.js
export const PI = 3.14159;
export const add = (a, b) => {
return a + b;
};
export const formatCurrency = (amount, currency = "USD") => {
return `${currency} ${amount.toFixed(2)}`;
};
// Default export (one per file)
const calculateTax = (amount, rate = 0.1) => {
return amount * rate;
};
export default calculateTax;
Now, in another file, main.js, we can import and use that code:
// File: main.js
// Named imports - specify what you want inside curly braces
import { PI, add, formatCurrency } from "./utils.js";
// Default import - no curly braces needed
import calculateTax from "./utils.js";
// You can also import everything
import * as Utils from "./utils.js";
console.log("The value of PI is:", PI); // Outputs: The value of PI is: 3.14159
console.log("2 + 3 is:", add(2, 3)); // Outputs: 2 + 3 is: 5
console.log(formatCurrency(99.99)); // Outputs: USD 99.99
console.log("Tax on $100:", calculateTax(100)); // Outputs: Tax on $100: 10
// Using the namespace import
console.log("PI from namespace:", Utils.PI);
console.log("Addition:", Utils.add(5, 7));
Named Imports Look Like Destructuring
Named imports use curly braces, so they look just like destructuring (though technically they are their own syntax):
// Pick exactly the pieces of the library you need
import { useState, useEffect } from "@odoo/owl";
Real-World OWL Import Example
Here's what a typical OWL component file looks like:
// File: my_component.js
// Import the base Component class and hooks
import { Component, useState, useService } from "@odoo/owl";
// Import utility functions from other files
import { formatDate, validateEmail } from "./utils.js";
// Define and export our component
export class MyComponent extends Component {
static template = "my.Component";
setup() {
// useState returns a reactive object (not a [value, setter] pair like React)
this.state = useState({
email: "",
isValid: false
});
this.orm = useService("orm");
// Use imported utility functions
this.validateAndSetEmail = (email) => {
this.state.email = email;
this.state.isValid = validateEmail(email);
};
}
}
// Export for use in other modules
export default MyComponent;
When you see import { Component } from "@odoo/owl"; at the top of an OWL file, you are simply importing the base Component class from the OWL library module so you can extend it.
Asynchronous JavaScript: Handling Delays
The toughest but most important topic is asynchronous code. Not all tasks are instant. When you ask the Odoo server for data (an RPC call), there's a delay while the request travels over the network and the server processes it.
If JavaScript waited for the response, your entire application—the whole user interface—would freeze. This is called "blocking" code, and it's a terrible user experience.
Asynchronous code allows JavaScript to start a long-running task (like a network request), and then continue with other work. When the task finally finishes, it notifies your code so you can handle the result.
The Old Way: Promises
The original solution to this was an object called a Promise. A Promise is an object that represents a future value. You attach .then() to handle a successful result and .catch() to handle an error.
// This is a simplified example of fetching data.
fetch('https://api.example.com/data')
.then(response => {
// This code runs when the server responds successfully.
return response.json(); // Get the data from the response.
})
.then(data => {
// This code runs after the data has been processed.
console.log("Data received:", data);
})
.catch(error => {
// This code runs if anything went wrong.
console.error("Failed to fetch data:", error);
});
console.log("Request sent! Waiting for response...");
The Modern Way: async/await
While Promises work, chaining .then() can get complicated. Modern JavaScript introduced a much cleaner syntax called async/await. It lets you write asynchronous code that looks like normal, synchronous code.
async: You put this keyword before a function declaration to tell JavaScript it contains asynchronous operations.await: You use this keyword inside anasyncfunction to tell JavaScript to pause execution on that line until the Promise resolves, without freezing the UI.
Here is the same example using async/await. This is the syntax you will use for all RPC calls in OWL:
// We define an async function to hold our logic.
const fetchData = async () => {
try {
// The 'await' keyword pauses the function here until the fetch is complete.
const response = await fetch('https://api.example.com/data');
// Once the response is back, the function continues.
const data = await response.json();
console.log("Data received:", data);
// You can return the data for use elsewhere
return data;
} catch (error) {
// If any 'await' call fails, the catch block will run.
console.error("Failed to fetch data:", error);
throw error; // Re-throw if needed
}
};
fetchData();
console.log("Request sent! Waiting for response...");
Real-World Async Patterns for OWL
Here are common asynchronous patterns you'll use in OWL development:
1. Loading data on component startup:
class MyComponent extends Component {
setup() {
this.state = useState({
isLoading: true,
records: [],
error: null
});
this.loadData();
}
async loadData() {
try {
// searchRead returns an array of record objects
const records = await this.orm.searchRead("res.partner", [], ["name", "email"]);
this.state.isLoading = false;
this.state.records = records;
} catch (error) {
this.state.isLoading = false;
this.state.error = error.message;
}
}
}
2. Handling user actions asynchronously:
async onSaveClick() {
const { name, email } = this.state.formData;
try {
// Show loading state
this.state.isSaving = true;
// Save to server (create returns an array of new ids)
const [recordId] = await this.orm.create("res.partner", [{ name, email }]);
// Update UI with success
this.state.isSaving = false;
this.state.lastSavedId = recordId;
// Show notification
this.notification.add("Record saved successfully!", { type: "success" });
} catch (error) {
this.state.isSaving = false;
this.notification.add(`Error: ${error.message}`, { type: "danger" });
}
}
3. Using destructuring with async functions:
async fetchCustomerData(customerId) {
try {
// Destructure the response directly
const [customer] = await this.orm.read("res.partner", [customerId], ["name", "email", "phone"]);
const { name, email, phone = "No phone provided" } = customer;
return {
displayName: `${name} (${email})`,
contactInfo: phone,
isComplete: email && phone !== "No phone provided"
};
} catch (error) {
console.error(`Failed to fetch customer ${customerId}:`, error);
return null;
}
}
This async/await syntax with a try...catch block is the standard, modern way to handle any operation that involves a delay, and it's fundamental to communicating with the Odoo server from your OWL components.
Putting It All Together: A Complete Example
Let's combine all the concepts from this chapter in a realistic OWL-style example. We'll build it up method by method rather than all at once.
First, the imports and the setup() method, which prepares the component's services and initial state, then kicks off loading data from the server:
// File: customer-manager.js
// Module imports with destructuring
import { Component, useState, useService } from "@odoo/owl";
import { formatDate, validateEmail } from "./utils.js";
export class CustomerManager extends Component {
static template = "CustomerManager";
setup() {
// Destructure services
const orm = useService("orm");
const notification = useService("notification");
// Set up state with destructuring assignment
this.state = useState({
customers: [],
selectedCustomer: null,
isLoading: false,
filters: { activeOnly: true, hasEmail: false }
});
// Store services for use in methods
this.orm = orm;
this.notification = notification;
// Load initial data
this.loadCustomers();
}
Next, loadCustomers() shows the full async/await + try/catch pattern from this chapter, combined with destructuring to read the active filters and the rest/spread pattern to enrich each record with extra display fields:
async loadCustomers() {
try {
// Destructure filters from state
const { activeOnly, hasEmail } = this.state.filters;
this.state.isLoading = true;
// Build domain based on filters
const domain = [];
if (activeOnly) domain.push(["active", "=", true]);
if (hasEmail) domain.push(["email", "!=", false]);
// searchRead returns an array of record objects
const records = await this.orm.searchRead(
"res.partner",
domain,
["name", "email", "phone", "active", "create_date"]
);
const totalCount = records.length;
// Transform the data using array methods and destructuring
const processedCustomers = records.map(customer => {
const { name, email, phone, create_date } = customer;
return {
...customer, // Spread original properties
displayName: `${name}${email ? ` (${email})` : ''}`,
formattedDate: formatDate(create_date),
isValidEmail: email ? validateEmail(email) : false,
hasContact: !!(email || phone)
};
});
// Update state
this.state.customers = processedCustomers;
this.state.isLoading = false;
this.notification.add(
`Loaded ${totalCount} customers successfully`,
{ type: "success" }
);
} catch (error) {
this.state.isLoading = false;
console.error("Failed to load customers:", error);
this.notification.add(
`Error loading customers: ${error.message}`,
{ type: "danger" }
);
}
}
onCustomerSelect() is smaller: it just demonstrates using .find() with a destructured parameter (({ id }) => ...) to locate a record by id:
async onCustomerSelect(customerId) {
// Find customer using array method with destructuring
const selectedCustomer = this.state.customers.find(({ id }) => id === customerId);
if (selectedCustomer) {
this.state.selectedCustomer = selectedCustomer;
// Destructure for logging
const { name, email } = selectedCustomer;
console.log(`Selected customer: ${name} (${email || 'no email'})`);
}
}
Finally, updateCustomerEmail() puts the rest pattern to use for real: it pulls id out on its own and keeps every other property in otherProps, so updating the email doesn't require re-listing every field:
// Class method using async/await and destructuring
async updateCustomerEmail(customerId, newEmail) {
try {
// Validate using imported utility
if (!validateEmail(newEmail)) {
throw new Error("Invalid email format");
}
// Update on server
await this.orm.write("res.partner", [customerId], { email: newEmail });
// Update local state using array map with destructuring
this.state.customers = this.state.customers.map(customer => {
const { id, ...otherProps } = customer;
if (id === customerId) {
return {
id,
...otherProps,
email: newEmail,
isValidEmail: true,
hasContact: true
};
}
return customer;
});
this.notification.add("Email updated successfully!", { type: "success" });
} catch (error) {
console.error("Failed to update email:", error);
this.notification.add(`Error: ${error.message}`, { type: "danger" });
}
}
}
// Default export
export default CustomerManager;
This example demonstrates: - Classes and inheritance (extending Component) - Destructuring in multiple contexts (imports, state, function parameters, array methods) - Modules (import/export) - Async/await for server communication - Template literals for string formatting - Modern array methods (map, find) combined with destructuring
These are the fundamental building blocks that make OWL components powerful and elegant. In the next chapter, we'll start building your first actual OWL component!
Common Pitfalls
Confusing spread and rest. Both use ..., but spread (on the right of =, building a new value: { ...customer, email }) expands properties out, while rest (on the left of =, in a destructuring pattern: const { id, ...rest } = customer) gathers leftover properties in. If your code isn't doing what you expect, check which side of the = your ... is on.
Calling this before super() in a subclass. In a class that extends another one, super(...) must be the first line of the constructor if you use this anywhere in it. JavaScript will throw a ReferenceError if you try to access this first.
Forgetting await on an async call. const records = this.orm.searchRead(...) (without await) gives you a pending Promise object, not the actual data. If you're trying to use .map() or .length on something and it's not working, check whether you forgot an await.
Not wrapping await in try/catch. If a promise rejects (e.g., the server returns an error) and there's no catch, you get an "unhandled promise rejection" and your component silently fails instead of showing useful feedback to the user.
Exercises
- Extend the
Puppyclass from this chapter with a new subclassServiceDogthat adds ataskproperty (e.g.,"guide","alert") and a methodperformTask()that logs"${name} is performing: ${task}". Make sure it callssuper()correctly. - Given
const order = { id: 5, product: "Widget", qty: 3, customer: { name: "Alice", email: "[email protected]" } };, destructureid,qty, and the nestedname(renamed tocustomerName) in a single statement. - Write an async function
checkStock(productId)that simulates a network call withawait new Promise(resolve => setTimeout(resolve, 500)), then returns{ productId, inStock: true }. Call it from another async function usingtry/catchand log the result.
TL;DR: OWL components are built entirely on class syntax, destructuring, ES modules (import/export), and async/await — master these four and there is no OWL syntax left that will surprise you.
Try it yourself: This chapter's example is available as an installable Odoo 19 addon: simplifyit_owl_book_ch4_ex1. Installation instructions are in the repository README.
What's Next?
With modern JavaScript in hand, Chapter 5 is where it all pays off: you'll write your first real OWL component, registered as a client action, and see this book's first line of actual framework code run in the browser.