RizTech Academy logo
RizTech Academy
Classes and ErrorsLesson 1 of 330 min

Classes, constructors and methods

Module 4 built orders as plain objects. That works until you have fifty of them and every one needs a total, a description and a validity check — at which point you are copying the same three functions into every object, or passing objects into loose functions and hoping they have the right shape.

A class is a template: define the shape and the behaviour once, stamp out as many as you need.

Defining one

class Order {
  constructor(customer, plates) {
    this.customer = customer;
    this.plates = plates;
  }

  describe() {
    return `${this.customer}: ${this.plates} plates`;
  }
}

const order = new Order('Priya', 3);

console.log(order.describe());
Priya: 3 plates

new does four things, and knowing them stops classes being magic:

  1. Creates a fresh empty object.
  2. Sets this to that object.
  3. Runs the constructor body.
  4. Returns the object — you do not write return.

constructor is a reserved name, runs once per object, and is where the per-instance data is set. Methods go beside it, with no function keyword and no commas between them — a class body is not an object literal, and treating it like one is the commonest syntax mistake.

Class names are PascalCase by convention, which is why module 2 said to keep that spelling for classes and camelCase for everything else.

new is not optional

const order = Order('Priya', 3);
TypeError: Class constructor Order cannot be invoked without 'new'

A helpfully specific error. Older constructor functions failed silently here, which is one of several reasons classes replaced them.

Classes are also not hoisted usefully:

new Later();
class Later {}
ReferenceError: Cannot access 'Later' before initialization

The same temporal dead zone as let and const, from module 3. Define before you use.

Getters: values that are computed

A total should never be stored — it would go stale the moment the plate count changed. A getter computes it on every read while looking like a property:

class Order {
  constructor(customer, plates) {
    this.customer = customer;
    this.plates = plates;
  }

  get total() {
    return this.plates * 80;
  }
}

const order = new Order('Priya', 3);

console.log(order.total);
order.plates = 5;
console.log(order.total);
240
400

No brackets. order.total reads as data and is calculated fresh each time.

A setter runs code on assignment, which is where validation and cleaning live:

class Order {
  set note(text) {
    this._note = text.trim();
  }

  get note() {
    return this._note;
  }
}

Use a getter whenever a value is derived from other values. Storing total alongside plates means two sources of truth, and they will disagree.

Private fields

A field starting with # is genuinely inaccessible from outside:

class Order {
  #note = '';

  set note(text) {
    this.#note = text.trim();
  }

  get note() {
    return this.#note;
  }
}

const order = new Order();
order.note = '  no chilli  ';

console.log(order.note);
console.log(order.#note);
no chilli
SyntaxError: Private field '#note' must be declared in an enclosing class

Not a convention — a rule the language enforces, and at parse time, so it is a SyntaxError rather than a runtime one. Private fields must be declared in the class body before use.

This is the same privacy closures gave you in module 3. Closures do it with scope, classes with #. Closures are lighter for one object; classes are better when you need many of the same thing.

Static members

static belongs to the class, not to any instance:

class Order {
  static rate = 80;
  static count = 0;

  constructor(customer, plates) {
    this.customer = customer;
    this.plates = plates;
    Order.count += 1;
  }

  get total() {
    return this.plates * Order.rate;
  }

  static compare(a, b) {
    return a.plates - b.plates;
  }
}

new Order('Priya', 3);
new Order('Arjun', 1);

console.log(Order.count);
2

Shared configuration and helpers that do not belong to one order. Order.compare is exactly the comparison function sort wanted in module 4, kept where it makes sense.

The reference

Piece Syntax Notes
Constructor constructor(a) { } Runs on new. One per class.
Method describe() { } No function, no commas.
Getter get total() { } Read without brackets.
Setter set note(v) { } Runs on assignment.
Public field plates = 0 Declared outside the constructor.
Private field #note = '' Must be declared. SyntaxError outside.
Static static rate = 80 On the class, not the instance.

The trap: methods are not arrow functions, and this can be lost

Everything from module 3 applies, and classes are where it bites hardest:

const order = new Order('Priya', 3);
const describe = order.describe;
describe();
TypeError: Cannot read properties of undefined (reading 'customer')

Detached from the object, so this is undefined — and class bodies are always strict mode, so it throws rather than quietly using the global object.

You will hit this passing a method to an event listener:

button.addEventListener('click', order.describe);

Three fixes, in order of preference:

button.addEventListener('click', () => order.describe());
button.addEventListener('click', order.describe.bind(order));
class Order {
  describe = () => {
    return `${this.customer}: ${this.plates} plates`;
  };
}

The third is a class field holding an arrow function, which takes this from the instance and cannot be detached. It costs one copy of the function per object rather than one shared on the prototype — which is the next lesson.

What JSON does to an instance

Worth knowing before it surprises you:

console.log(JSON.stringify(new Order('Priya', 3)));
{"customer":"Priya","plates":3}

Own data only. Methods, getters and private fields are all gone — a getter is not a property, so total does not appear. Parse that back and you have a plain object that looks like an order and has none of its behaviour. Rebuild with new Order(...) if you need the methods.

When not to use a class

Honest guidance, because classes are overused by people arriving from Java.

A class earns its place when you have many things of the same shape with behaviour attached, or when you need to model a hierarchy. An order, a validator, a custom error — all reasonable.

A plain object is better for data, and most of what you handle is data. An API response is a plain object. A configuration is a plain object. A single thing that exists once does not need a template.

A function is better than a class with one method. A class named PriceCalculator whose only method is calculate is a function wearing a costume.

Modern React and most front-end JavaScript use very few classes. You need them to read code, to write custom errors — the third lesson of this module — and occasionally to model something genuinely object-shaped.

Check your work

new Order('Priya', 3) then .describe() gives Priya: 3 plates. new creates the object, sets this, runs the constructor and returns it.

Calling a class without new throws TypeError: Class constructor Order cannot be invoked without 'new'.

Using a class above its definition throws ReferenceError: Cannot access 'Later' before initialization — the same dead zone as let.

order.total is read without brackets and recalculates. Change plates from 3 to 5 and it goes 240 to 400. That is the reason to compute rather than store.

order.#note from outside is a SyntaxError, not a runtime error — private fields are enforced at parse time and must be declared in the class body.

Order.count after two new Order(...) calls is 2. A static field lives on the class and is shared.

A detached method loses this and throws, because class bodies are strict. Fix with an arrow wrapper, bind, or a class field holding an arrow.

JSON.stringify(instance) gives {"customer":"Priya","plates":3} — own data only, no methods, no getters, no private fields.

The Subscription class keeps its balance and history private and hands back a copy:

class Subscription {
  #balance;
  #rate;
  #history = [];

  constructor(balance, rate) {
    this.#balance = balance;
    this.#rate = rate;
  }

  get balance() {
    return this.#balance;
  }

  get history() {
    return [...this.#history];
  }

  deliver(plates) {
    const cost = plates * this.#rate;
    if (cost > this.#balance) return 'Insufficient balance.';
    this.#balance -= cost;
    this.#history.push({ plates, cost });
    return this.#balance;
  }
}

return [...this.#history] matters: returning the array itself would let a caller push to your private list, exactly as in module 3.

Class or closure? The closure version from module 3 does the same job in fewer concepts and is the better choice for one subscription. The class wins once you need many of them, or a Subscription that extends something — and it is what most codebases will hand you, so you need to read both.

Practice

  1. Write an Order class with a constructor, a describe() method, and create two instances.
  2. Call the class without new and read the error.
  3. Use a class before its definition and read the error. Compare it with the let error from module 3.
  4. Add a total getter. Change plates afterwards and confirm the total follows.
  5. Store the total instead, as this.total = plates * 80 in the constructor. Change plates and watch the total go stale. Then go back to the getter.
  6. Add a #note private field with a getter and setter that trims. Try to read #note from outside and read the error.
  7. Add static rate and static count, and confirm the count rises with each instance.
  8. Lose this. Assign a method to a variable, call it, read the error. Fix it all three ways and decide which you would use.
  9. JSON.stringify an instance and account for everything missing from the output.
  10. Harder. Write a Subscription class holding a balance, a rate and a delivery history. deliver(plates) should refuse when the balance is too low and otherwise record the delivery; history should return the deliveries without letting a caller modify the stored list. Then write the same thing as a closure-based factory from module 3, and say which you would rather maintain and why.

Next: prototypes — what a class actually is underneath, and how extends works.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.

About the internship