JavaScript Engineering

Prototype Pitfalls That Cause Production Bugs

Lost this context in callbacks, state that leaks across every instance of a class, constructors called without new, these aren't edge cases. They're some of the most common bugs in real JavaScript codebases.

April 2026 10 min read JavaScript  ·  Prototypes  ·  this
Start Reading Back to Blog

In This Article

Four real prototype and this-related bugs: losing context when methods are passed as callbacks, shared mutable state via the prototype, forgetting new, and prototype pollution, each with the fix.

Need Help?

Working through a tricky JavaScript architecture or performance problem? Let's talk.

Talk to an Engineer
Deep Dive

Four Prototype Bugs, and How to Avoid Them

Master JavaScript's most flexible features, April 2026  ·  10 min read

These Bugs Compile Fine. They Fail at Runtime.

Prototype and this-related bugs share one trait: the code looks correct, passes a quick manual test, and then breaks in a specific circumstance, a callback, a second instance, a missing keyword, that doesn't show up until production.


Pitfall 01, Losing this in a Callback

The Most Common this Bug in Real Code

this is determined by how a function is called, not where it's defined. Passing a method as a callback separates it from the object it belongs to, and this goes with it.

class OrderService {
    constructor() {
        this.pendingOrders = [];
    }

    handleNewOrder(order) {
        this.pendingOrders.push(order); // relies on 'this'
    }
}

const service = new OrderService();

// Looks fine...
socket.on('order:created', service.handleNewOrder);
// TypeError: Cannot read properties of undefined (reading 'pendingOrders')
// 'this' inside handleNewOrder is no longer 'service'

service.handleNewOrder extracts the function itself, stripped of the object it was called on. When the event emitter later calls it as a plain function, this is undefined (in strict mode / ES modules) or the global object (in sloppy mode), neither of which has pendingOrders.

The fix, three ways:
// 1. Arrow function wrapper — captures 'this' at the call site
socket.on('order:created', (order) => service.handleNewOrder(order));

// 2. .bind() — permanently locks 'this' to service
socket.on('order:created', service.handleNewOrder.bind(service));

// 3. Class field arrow function — bound once, at construction
class OrderService {
    handleNewOrder = (order) => {
        this.pendingOrders.push(order);
    };
}

Pitfall 02, Shared Mutable State on the Prototype

One Array, Every Instance

Properties assigned directly on a prototype (rather than inside the constructor) are shared by every instance. For primitives this rarely matters. For objects and arrays, it's a classic source of "why does updating one user update all of them" bugs.

function ShoppingCart() {}
ShoppingCart.prototype.items = []; // shared across every cart!

const cartA = new ShoppingCart();
const cartB = new ShoppingCart();

cartA.items.push('laptop');
console.log(cartB.items); // ['laptop'] — cartB never touched this

Both instances read items through the prototype chain, and since arrays are mutated in place, pushing onto cartA.items mutates the one shared array both instances point to. This exact bug appears with ES6 classes too, if a field is initialized outside the constructor with a mutable default.

The fix: initialize objects and arrays inside the constructor (or as class fields with = [] per-instance, which class syntax handles correctly), never on the shared prototype.
function ShoppingCart() {
    this.items = []; // new array, per instance
}

Pitfall 03, Forgetting new

A Silent Corruption, Not a Crash
function Order(total) {
    this.total = total;
    this.status = 'pending';
}

// Forgot 'new'
const order = Order(299);

console.log(order);        // undefined — Order returns nothing
console.log(globalThis.total); // 299 in sloppy mode — leaked to global scope

Without new, Order runs as a plain function call. this isn't bound to a new object, it falls back to the global object (sloppy mode) or undefined (strict mode / classes). In sloppy mode, this doesn't throw, it silently pollutes global state, exactly the implicit-global problem, caused by a missing keyword instead of a missing declaration.

The fix: use ES6 class syntax instead of constructor functions. Classes throw a TypeError if called without new, turning a silent corruption into an immediate, loud failure.
class Order {
    constructor(total) {
        this.total = total;
    }
}

Order(299); // TypeError: Class constructor Order cannot be invoked without 'new'

Pitfall 04, Prototype Pollution

Modifying Built-Ins Affects Everything

Adding properties to a built-in prototype, Array.prototype, Object.prototype, changes the behavior of every array or object in the entire application, including third-party libraries.

// Seems convenient...
Array.prototype.last = function () {
    return this[this.length - 1];
};

[1, 2, 3].last(); // 3

// But now every for...in loop over every array in the codebase
// (including inside your dependencies) sees 'last' as an enumerable property
for (const key in [1, 2, 3]) {
    console.log(key); // '0', '1', '2', 'last'  ← unexpected
}

Beyond the immediate bug, this is a well-known attack surface: if user-controlled data is merged into an object carelessly (a common bug in older deep-merge or config-parsing utilities), an attacker can inject a __proto__ key and modify Object.prototype for the entire running process.

The fix: never modify built-in prototypes. Write standalone utility functions instead, and validate or sanitize any object built from user input before merging it into application state.
// Standalone function, no global side effects
function last(arr) {
    return arr[arr.length - 1];
}
Final Takeaway

All four of these bugs come from the same root cause: this and the prototype chain are resolved dynamically, at call time, not where the code is written. Understanding that isn't academic, it's what separates code that works in a quick test from code that survives production.

Topics

JavaScript Prototypes this ES6 Classes

Related Articles

Higher-Order Functions That Simplify Enterprise JavaScript

Middleware chains, composition, memoization, and the closures that power them.

JavaScript Scoping Bugs We've Seen in Production

Five real scoping bugs, from the var-in-a-loop trap to prototype pollution.

Where Should JWT Validation Happen?

Trust boundaries across four production patterns in microservices.