Skip to Content
  • Home
  • Book
  • Blog
  • About us
  • Help
  • Contact us
  •  [email protected]
  • Sign in
  • English (US) Español (BO)
  • Simplify It S.R.L.
    • Contact Us
Simplify It S.R.L.
      • Home
      • Book
      • Blog
      • About us
      • Help
      • Contact us
    •  [email protected]
    • English (US) Español (BO)
    • Sign in
    • Contact Us

    Chapter 4: Modern JavaScript Fundamentals, Part II

  • All Blogs
  • OWL Book
  • Chapter 4: Modern JavaScript Fundamentals, Part II
  • August 25, 2026 by
    Chapter 4: Modern JavaScript Fundamentals, Part II
    Grover Menacho

    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 **/ and import/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: The async/await and try/catch patterns 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 an async function 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

    1. Extend the Puppy class from this chapter with a new subclass ServiceDog that adds a task property (e.g., "guide", "alert") and a method performTask() that logs "${name} is performing: ${task}". Make sure it calls super() correctly.
    2. Given const order = { id: 5, product: "Widget", qty: 3, customer: { name: "Alice", email: "[email protected]" } };, destructure id, qty, and the nested name (renamed to customerName) in a single statement.
    3. Write an async function checkStock(productId) that simulates a network call with await new Promise(resolve => setTimeout(resolve, 500)), then returns { productId, inStock: true }. Call it from another async function using try/catch and 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.

    in OWL Book
    Chapter 5: Hello, OWL! Your First Component
    Reading the whole book? Take it with you — free, no sign-up.
    PDF EPUB All chapters
    Useful Links
    • Home
    • About us
    • Blog
    • The OWL 2.0 book
    • Help
    • Contact us
    About us

    Simplify It S.R.L. is an Odoo implementation and development firm based in La Paz, Bolivia, working with companies across Latin America and North America. We have been building on Odoo since version 6.1: custom modules, version migrations and Bolivian localization.

    We are also the authors of the OWL 2.0 book, published free chapter by chapter on our blog.

    Connect with us
    • Contact us
    • [email protected]
    • +591 65144144
    • La Paz, Bolivia
    Follow us
    Copyright © Simplify It S.R.L.
    English (US) | Español (BO)
    Powered by Odoo - Create a free website