JavaScript `new` Keyword: A Practical Deep Dive for Modern Codebases

You hit a bug at 11:40 PM, and it looks ridiculous: TypeError: Cannot read properties of undefined. You stare at the line and realize you forgot one tiny word before calling a constructor-style function. That one word is new, and when it’s missing, JavaScript shifts from object construction to plain function execution. I’ve seen this mistake from beginners, seniors, and even code generators that weren’t configured well.

If you write JavaScript seriously in 2026, you still need a sharp mental model of new, even if you mostly use class syntax. Under the hood, class instantiation still relies on the same construction mechanics. Understanding new helps you debug weird return values, prototype issues, memory growth, and behavior differences between factories and constructors. It also helps you review AI-suggested code faster, because many generated snippets still mix patterns incorrectly.

By the time you finish reading, you’ll know exactly what new does, when you should use it, when you should avoid it, and how to spot subtle bugs that pass code review but fail in production.

What new Actually Does (Step by Step)

When you call a function with new, JavaScript performs a sequence of actions. I recommend memorizing this sequence because it explains almost every constructor-related bug:

  • A fresh empty object is created.
  • That object’s internal prototype ([[Prototype]]) is linked to ConstructorFunction.prototype.
  • this inside the constructor is bound to the new object.
  • The constructor body runs.
  • The final returned value follows specific rules:

– If the constructor returns a non-primitive (object, array, function), that value is returned.

– If it returns nothing, JavaScript returns this.

– If it returns a primitive (number, string, boolean, null, undefined, symbol, bigint), JavaScript ignores it and returns this.

The syntax is straightforward:

new ConstructorFunction(arg1, arg2, arg3)

A constructor function can be a traditional function or a class constructor. The rules above apply in both cases, though classes enforce stricter behavior.

Here is a simple runnable example:

function Fruit(color, taste, seeds) {

this.color = color

this.taste = taste

this.seeds = seeds

}

const mango = new Fruit(‘Yellow‘, ‘Sweet‘, 1)

console.log(mango.color) // Yellow

console.log(mango.taste) // Sweet

console.log(mango.seeds) // 1

Think of new as a mini object factory protocol built into the language runtime. The function body gives the object data; the prototype link gives it shared behavior.

A useful mental shortcut I teach juniors: new is not just syntax sugar for calling a function. It is a different call mode. Same identifier, different runtime protocol.

Constructor Functions vs class in 2026

Most modern codebases use class, but I still review and maintain constructor functions regularly, especially in older packages, plugin systems, and generated SDKs.

Under the hood, class is still built on prototype-based construction. The biggest difference is ergonomics and safety.

Pattern

Without new

Prototype behavior

Typical 2026 usage

Constructor function

May silently bind wrong this (or global/undefined depending on mode)

Manual via Function.prototype

Legacy code, low-level libs

class constructor

Throws immediately (Class constructor cannot be invoked without new)

Automatic class prototype chain

Primary app-level patternExample comparison:

function AccountFunction(owner) {

this.owner = owner

}

class AccountClass {

constructor(owner) {

this.owner = owner

}

}

const a1 = new AccountFunction(‘Nina‘)

const a2 = new AccountClass(‘Nina‘)

console.log(a1.owner) // Nina

console.log(a2.owner) // Nina

If you call AccountFunction(‘Nina‘) without new, behavior depends on strict mode and call site. If you call AccountClass(‘Nina‘) without new, it throws right away. That fail-fast behavior is one reason I recommend class for most product code.

Still, constructor functions remain important because many tools, older libraries, and metaprogramming helpers expose them directly. If you understand new, you understand both worlds.

In practice, I choose based on team consistency:

  • If the codebase is mostly classes, I stay class-first.
  • If the package exposes plain factories, I avoid introducing constructors randomly.
  • If public API compatibility matters, I preserve existing call style and add guards.

Return Values: The Part Most Developers Misremember

I test this in interviews because even experienced developers get it wrong.

Case 1: Returns nothing

function Session(userId) {

this.userId = userId

// no explicit return

}

const s = new Session(‘user_42‘)

console.log(s.userId) // user_42

JavaScript returns the constructed object (this).

Case 2: Returns a primitive

function Token(kind) {

this.kind = kind

return ‘OVERRIDE_ATTEMPT‘

}

const t = new Token(‘access‘)

console.log(t.kind) // access

The primitive return is ignored.

Case 3: Returns an object

function Profile(name) {

this.name = name

return { name: ‘Replacement‘, source: ‘custom-return‘ }

}

const p = new Profile(‘Original‘)

console.log(p.name) // Replacement

console.log(p.source) // custom-return

The explicit object return replaces this.

I strongly advise against returning replacement objects from constructors unless you have a very specific reason. It creates confusing behavior, breaks expectations for readers, and can invalidate instanceof checks depending on what you return.

Here is a tricky but educational snippet:

function Car(model) {

this.model = model

return { model: ‘Injected‘ }

}

const car = new Car(‘Roadster‘)

console.log(car.model) // Injected

console.log(car instanceof Car) // false (usually)

That instanceof result surprises teams during debugging. You think you built a Car; you actually returned a plain object.

One more subtle edge case: returning a function also overrides the instance, because functions are objects in JavaScript.

function WeirdThing() {

this.name = ‘instance‘

return function hello() {}

}

const w = new WeirdThing()

console.log(typeof w) // function

console.log(w.name) // hello (function name), not ‘instance‘

This is legal, and that is exactly why it is dangerous.

Prototype Linking and Shared Methods

The second step of new—linking the object to Constructor.prototype—is where shared behavior lives.

function Order(orderId, amount) {

this.orderId = orderId

this.amount = amount

}

Order.prototype.format = function () {

return Order ${this.orderId}: $${this.amount}

}

const firstOrder = new Order(‘A100‘, 49)

const secondOrder = new Order(‘A101‘, 79)

console.log(firstOrder.format())

console.log(secondOrder.format())

console.log(firstOrder.format === secondOrder.format) // true

Both objects share one format function via prototype, which saves memory and keeps behavior consistent.

Now compare that with defining methods inside the constructor:

function OrderHeavy(orderId, amount) {

this.orderId = orderId

this.amount = amount

// New function object created per instance

this.format = function () {

return Order ${this.orderId}: $${this.amount}

}

}

const o1 = new OrderHeavy(‘B100‘, 20)

const o2 = new OrderHeavy(‘B101‘, 30)

console.log(o1.format === o2.format) // false

Per-instance method definitions can be valid for closures and private state, but they increase memory usage. In UI-heavy apps with thousands of objects, I often see meaningful memory differences. In practical terms, shared prototype methods typically reduce per-instance overhead noticeably, while constructor-bound methods increase allocation pressure and garbage collection frequency.

Prototype example matching the classic pattern

function Record() {

let localCounter = 1 // not attached to this

this.id = 100

}

Record.prototype.version = 200

const record = new Record()

console.log(record.id) // 100 (own property)

console.log(record.version) // 200 (from prototype)

console.log(record.localCounter) // undefined

Only properties assigned to this become instance-own properties. Local variables inside the constructor are private to that invocation and not visible on the object.

A quick debugging pattern I use:

console.log(Object.keys(record)) // own enumerable keys

console.log(Object.getPrototypeOf(record)) // prototype object

If a method is missing, this tells me whether the issue is instance state or prototype chain.

new.target, Strict Mode, and Safety Guards

If you must support constructor-style functions in shared code, guard them.

new.target tells you whether a function was called with new.

function Invoice(total) {

if (!new.target) {

throw new Error(‘Invoice must be called with new‘)

}

this.total = total

}

const inv = new Invoice(500)

console.log(inv.total) // 500

This gives you class-like safety while staying in function form.

I also recommend strict mode in legacy constructor modules:

‘use strict‘

function Customer(name) {

this.name = name

}

Without strict mode, an accidental call without new can write to the global object in older patterns. With strict mode, this becomes undefined in plain calls, making failures easier to catch.

In modern ESM files, strict mode is already active by default, but old CommonJS files and embedded scripts still show up in enterprise environments.

Another guard I like in mixed codebases:

function User(name) {

if (!(this instanceof User)) {

return new User(name)

}

this.name = name

}

I only use this for compatibility layers, not new code. It is forgiving, but can hide misuse that should fail loudly.

Common Mistakes I Keep Seeing in Real Code Reviews

1) Forgetting new

function Payment(amount) {

this.amount = amount

}

const p = Payment(99) // mistake

console.log(p) // undefined in strict mode

Fix: use new Payment(99) or convert to a factory function explicitly.

2) Using arrow functions as constructors

const Person = (name) => {

this.name = name

}

// const user = new Person(‘Ava‘) // TypeError: Person is not a constructor

Arrow functions do not have [[Construct]]; they cannot be used with new.

3) Returning replacement objects from constructors

As shown earlier, this breaks expectations and can fail type checks.

4) Mixing factory and constructor patterns

function createCache(size) {

return { size }

}

// new createCache(10) // legal in some cases but semantically confusing

If it’s a factory, name it like a factory and call it plainly. If it’s a constructor, capitalize and call with new.

5) Reassigning prototype after instances exist

function User(name) {

this.name = name

}

const oldUser = new User(‘Lena‘)

User.prototype = {

greet() {

return Hi ${this.name}

}

}

const newUser = new User(‘Arman‘)

console.log(typeof oldUser.greet) // undefined

console.log(newUser.greet()) // works

Existing instances keep the old prototype chain. If you patch prototypes late, behavior diverges across objects.

6) Forgetting constructor when replacing prototype object

If you fully replace User.prototype, you also replace the default constructor reference. That can confuse reflection code.

function User(name) {

this.name = name

}

User.prototype = {

greet() {

return Hi ${this.name}

}

}

const u = new User(‘Mina‘)

console.log(u.constructor === User) // false in this pattern

7) Shadowing prototype methods with own properties

function Timer() {}

Timer.prototype.start = function () {

return ‘started‘

}

const t = new Timer()

t.start = ‘oops‘

console.log(typeof t.start) // string, method is shadowed

This kind of bug appears in long-lived objects after unrelated refactors.

When You Should Use new (and When You Should Not)

I recommend this decision guide for day-to-day work.

Use new when:

  • You define a type with identity and lifecycle (for example Cart, Order, Session).
  • You need shared behavior through prototype/class methods.
  • You want instanceof checks and clear object semantics.
  • You’re implementing APIs that are naturally instance-based.

Avoid new when:

  • You only need plain data shaping.
  • You prefer immutable object creation and pure functions.
  • You return specialized objects conditionally (factory pattern).
  • You’re writing small utility modules where stateful instances add noise.

Factory example (no new needed):

function createTheme(name, darkMode) {

return {

name,

darkMode,

createdAt: new Date()

}

}

const theme = createTheme(‘Ocean‘, true)

console.log(theme.name)

Constructor/class example (good new use):

class WebSocketClient {

constructor(url) {

this.url = url

this.connected = false

}

connect() {

this.connected = true

}

}

const client = new WebSocketClient(‘wss://example.dev/socket‘)

client.connect()

console.log(client.connected) // true

The key is intent clarity. If readers can’t tell whether a callable creates instances or returns computed data, maintenance slows down quickly.

A naming convention that works well in teams:

  • Constructor/class: PascalCase + new
  • Factory: createSomething, buildSomething, makeSomething
  • Never mix both styles for the same exported symbol

new in the 2026 Tooling Era

Even with AI pair-programming everywhere, new mistakes still appear often in generated code. I catch these patterns repeatedly:

  • Constructor-like function names called without new.
  • Factory functions capitalized like constructors.
  • Prototype method definitions mixed with class fields in awkward ways.
  • Returned object overrides in constructors, which make type systems unhappy.

How I keep this clean in real teams:

  • ESLint rules that enforce constructor conventions (new-cap, no-invalid-this, custom architecture rules).
  • TypeScript strict mode so constructor signatures stay explicit.
  • Code review checklist item: Is this callable a factory, constructor, or class?
  • Unit tests that verify instanceof, own keys, and prototype method availability.

A minimal test pattern:

class Cart {

constructor() {

this.items = []

}

add(item) {

this.items.push(item)

}

}

const cart = new Cart()

cart.add(‘Notebook‘)

console.log(cart instanceof Cart) // true

console.log(Object.hasOwn(cart, ‘items‘)) // true

console.log(typeof cart.add === ‘function‘) // true (from prototype)

When you wire this into CI, you catch constructor misuse before it hits runtime.

Performance-wise, object construction cost is usually tiny for normal business paths, but pattern choice matters at scale. In benchmarks with large object counts, using prototype/shared class methods often cuts function allocation overhead enough to reduce execution time by a visible margin (typically single-digit to low double-digit percentages) and smooth garbage collection pauses. You don’t need micro-bench obsession; you do need consistent patterns.

Advanced Edge Cases You’ll Actually Hit

Most tutorials stop at basics. Real projects do not.

new with bound functions

You can call new on a bound function if the original target is constructable.

function Point(x, y) {

this.x = x

this.y = y

}

const YFixedPoint = Point.bind(null, 10)

const p = new YFixedPoint(20)

console.log(p.x, p.y) // 10 20

console.log(p instanceof Point) // true

Important detail: binding thisArg is ignored during construction; the newly created object becomes this.

Operator precedence gotchas

These two lines do not mean the same thing:

new Date().getTime()

new Date.getTime()

new Date().getTime() constructs a Date, then calls getTime.
new Date.getTime() tries to construct using Date.getTime as a constructor (usually wrong).

I see this in minified code and rushed refactors.

new cannot be optional chained

You cannot do this:

// new maybeCtor?.()

If constructor existence is dynamic, choose a factory wrapper or explicit conditional.

Reflect.construct

If you build frameworks, metaprogramming tools, or DI containers, Reflect.construct gives more control than plain new.

function Animal(name) {

this.name = name

}

function Dog(name) {

this.kind = ‘dog‘

}

const d = Reflect.construct(Animal, [‘Milo‘], Dog)

console.log(d.name) // Milo

console.log(Object.getPrototypeOf(d) === Dog.prototype) // true

This is advanced, but useful when you need to separate initializer and prototype target.

Inheritance, super(), and new

When you instantiate a derived class, new triggers constructor flow across the inheritance chain.

class Vehicle {

constructor(id) {

this.id = id

}

}

class Car extends Vehicle {

constructor(id, model) {

super(id)

this.model = model

}

}

const c = new Car(‘v-1‘, ‘Roadster‘)

console.log(c.id, c.model)

In derived classes, you must call super() before using this. That rule exists because the base constructor participates in creating and initializing the instance.

I treat this as a practical debugging checklist:

  • If this errors inside subclass constructor, check missing super().
  • If inherited methods are missing, inspect prototype chain with Object.getPrototypeOf.
  • If class fields seem undefined, verify execution order between base and derived initialization.

Built-in Constructors: Where new Is Required, Optional, or Misleading

This is where many developers get tripped up.

Built-in

Typical call style

Result shape —

Date

new Date() (recommended)

Date object Array

new Array() or []

Array object Map

new Map()

Map object Set

new Set()

Set object Promise

new Promise(...)

Promise object Object

new Object() or {}

Plain object Number

Number(value) preferred

Primitive number String

String(value) preferred

Primitive string Boolean

Boolean(value) preferred

Primitive boolean

Two practical warnings:

  • new Boolean(false) creates an object wrapper that is truthy, which causes painful conditional bugs.
  • new Number(0) and new String(‘x‘) create wrapper objects, not primitives.

Example:

const a = Boolean(false)

const b = new Boolean(false)

console.log(a) // false

console.log(Boolean(b)) // true, because objects are truthy

For wrapper types, I almost always call them without new.

Performance and Memory: Practical Guidance, Not Micro-Benchmark Theater

I care about new performance in two scenarios: high-churn hot paths and long-lived in-memory graphs.

Where costs come from

  • Instance property allocation
  • Per-instance function allocation (if methods created in constructor)
  • Hidden class shape churn when property order is inconsistent
  • Garbage collection from short-lived object bursts

What I do in production code

  • Keep method definitions on prototype/class body unless I need closure privacy.
  • Initialize instance properties in a stable order across all constructor paths.
  • Avoid conditional addition of many new keys after construction in hot loops.
  • Reuse objects only where profiling proves allocation churn hurts latency.

Realistic before/after pattern

Before (higher allocation pressure):

function Item(id) {

this.id = id

this.serialize = function () {

return { id: this.id }

}

}

After (shared method):

function Item(id) {

this.id = id

}

Item.prototype.serialize = function () {

return { id: this.id }

}

In object-heavy paths, this pattern often reduces memory growth slope and improves frame/response stability.

Debugging Playbook for new-Related Bugs

When I suspect new misuse, I run this sequence quickly:

  • Verify call site: is constructor invoked with new?
  • Log runtime type: typeof value, value.constructor?.name.
  • Check identity: value instanceof ExpectedType.
  • Inspect own vs prototype properties: Object.keys(value) + Object.getPrototypeOf(value).
  • Look for explicit return in constructor.
  • Confirm the constructor is not an arrow function.
  • Check whether prototype was reassigned after some instances were created.

I also add temporary assertions in tests:

expect(instance).toBeInstanceOf(Service)

expect(Object.hasOwn(instance, ‘state‘)).toBe(true)

expect(typeof instance.run).toBe(‘function‘)

These assertions catch regressions when someone silently converts class methods to per-instance closures or accidentally returns replacement objects.

Refactoring Patterns: Constructor Function to Class (Safely)

Many teams want to modernize legacy constructor functions without breaking behavior.

I use an incremental strategy:

  • Freeze behavior with tests (especially instanceof, serialization shape, error behavior).
  • Convert prototype methods to class methods one by one.
  • Keep constructor signature identical at first.
  • Avoid changing public field names in the same PR.
  • Release with compatibility notes if API is public.

Legacy form:

function Queue() {

this.items = []

}

Queue.prototype.enqueue = function (item) {

this.items.push(item)

}

Class form:

class Queue {

constructor() {

this.items = []

}

enqueue(item) {

this.items.push(item)

}

}

Same runtime idea, cleaner syntax, better tooling support.

If I want to go factory-first instead, I do it explicitly and rename API to prevent ambiguity.

Practical Scenarios: Choosing Between Constructor, Class, and Factory

I use this matrix in architecture reviews:

Scenario

Best fit

Why —

— Domain entities with methods (Order, Session)

Class/constructor + new

Stable identity, shared behavior Stateless value transformation

Factory/plain function

Simpler, test-friendly, no instance lifecycle Conditional subtype creation

Factory

Can choose implementation at runtime Plugin instances with lifecycle hooks

Class + new

Clear contracts and extensibility Tiny config object creation

Factory/object literal

Minimal ceremony

Concrete example: API client library

  • I prefer class for long-lived client with auth state, retries, and middleware.
  • I prefer factory for one-shot helpers like buildQueryParams.

Concrete example: UI state models

  • Class can work for local rich-domain models.
  • For global state stores and reducers, plain objects/functions are often cleaner.

Team Conventions That Prevent 80% of Bugs

These are low-cost and high-impact:

  • Enforce lint rules: new-cap, no-new-wrappers, no-invalid-this.
  • Use TypeScript constructor signatures for public APIs.
  • Ban ambiguous exports: no symbol used both as factory and constructor.
  • Add one review checklist line: Is call style intentional and consistent?
  • Document pattern per folder: domain uses classes, utils uses pure functions.

I also suggest one architectural guardrail: do not expose raw constructors from modules unless there is a real reason. Export named factories or classes intentionally, not both.

Putting It to Work This Week

If you want fewer object-model bugs and cleaner code reviews, start with three concrete moves. First, classify every callable in your module as constructor/class or factory, and rename anything ambiguous. Clear naming alone prevents a surprising number of bugs. Second, enforce call style with lint rules and tests so mistakes fail early. Third, keep methods on prototypes (or class methods) unless you truly need per-instance closures.

When you write constructor logic, remember the mental checklist: create object, link prototype, bind this, run constructor, resolve return rules. That single sequence explains instanceof behavior, missing properties, and why primitive returns are ignored. I still run this checklist mentally when debugging unfamiliar code.

You should also be strict about team conventions. If your team prefers classes, default to classes. If your team has established factory-heavy architecture, stay there and avoid half-converted constructor patterns. Consistency beats novelty every time.

The biggest payoff is confidence. Once new stops feeling magical, object construction becomes predictable. You can reason about memory usage, method sharing, and runtime behavior without guesswork. That makes refactoring safer, code generation easier to review, and production incidents much less dramatic. If you teach junior developers one JavaScript runtime concept this month, teach this one—they’ll use it in every serious codebase they touch.

Quick Reference Checklist

Before merging code that involves object creation, I quickly verify:

  • Constructor/class is called with new consistently.
  • Factories are named as factories and never capitalized like constructors.
  • No constructor returns replacement objects unless intentionally documented.
  • Methods that should be shared live on prototype/class body.
  • Tests assert both behavior and identity (instanceof, own keys, method availability).

If those five checks pass, most new-related production bugs disappear before release.

Scroll to Top