· 10 min read

Controversial JavaScript Questions: Testing Boundaries of Developer Knowledge

JavaScript debates are part technical, part cultural. This article walks through the most contentious questions - from == vs === and semicolons to TypeScript, immutability, eval, private fields and more - explains both sides, shows examples, and offers pragmatic guidance for teams and individual developers.

Why JavaScript Sparks So Many Strong Opinions

JavaScript is unique: it’s ubiquitous, evolving fast, surprisingly flexible, and has a huge ecosystem. That combination produces a lot of legitimate technical trade-offs and a lot of opinionated preferences. Some debates are purely stylistic; others can affect correctness, performance, maintainability, and security.

Below are some of the most controversial JavaScript questions you’ll encounter. For each: the question, why people argue, the technical trade-offs, code examples, and a practical recommendation.


1) == vs === - Is type-coercing equality ever acceptable?

Question: Use == (abstract equality) or always use === (strict equality)?

Why it’s controversial: == performs type coercion and can produce surprising results; defenders say it’s concise in some cases (e.g. checking null/undefined).

Example:

0 == '0'; // true
0 === '0'; // false
null == undefined; // true
null === undefined; // false

Arguments for ===:

  • Avoids implicit type coercion surprises
  • Easier to reason about
  • Many style guides require it (Airbnb, etc.)

Arguments for ==:

  • Concise checks like x == null match both null and undefined
  • Historical codebases use it; some consider it readable in small idioms

Recommendation: Prefer === for clarity and correctness. If you need to check for both null and undefined, write the intention explicitly: x == null is acceptable if documented, but x === null || x === undefined or x ?? defaultValue (nullish coalescing) is clearer.

References: MDN on equality operators: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Equality_comparisons_and_sameness


2) Semicolons: Omit them or enforce them?

Question: Rely on Automatic Semicolon Insertion (ASI) or always write semicolons?

Why it’s controversial: ASI generally works, but corner cases can silently change program behavior.

Problematic example when omitting semicolons:

// Intended to return an object
function getObj() {
  return;
  {
    ok: true;
  }
}

// Because of ASI, this returns undefined, not the object.

Arguments for omitting semicolons:

  • Cleaner visual appearance for some
  • Modern tools and linters handle most cases

Arguments for requiring semicolons:

  • Avoid subtle bugs from ASI
  • Uniformity across codebase

Recommendation: Use a consistent rule enforced by tooling (Prettier/ESLint). Many teams choose to always include semicolons to avoid rare pitfalls. See MDN on ASI: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#automatic_semicolon_insertion


3) var vs let/const - Are old habits safe?

Question: Is var still acceptable? When to use let vs const?

Why it’s controversial: var has function scoping and hoisting semantics that lead to bugs in modern code. let/const have block scoping and better intentions.

Example:

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}
// prints 3 3 3

// With let:
for (let j = 0; j < 3; j++) {
  setTimeout(() => console.log(j), 0);
}
// prints 0 1 2

Arguments for let/const:

  • Block scope prevents accidental leaks
  • const provides intent (value shouldn’t be reassigned)

Arguments sometimes used for var:

  • Historical legacy code
  • Slightly different hoisting semantics can be used intentionally (rare)

Recommendation: Favor const for values that won’t be reassigned, let for variables that will. Avoid var in new code. See MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/var and https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let


4) Arrow functions vs function declarations - Which to choose for this?

Question: Use arrow functions everywhere or prefer traditional functions in some contexts?

Why it’s controversial: Arrow functions capture the surrounding this lexically, which is convenient in callbacks but unsuitable for methods that should use their own this.

Example:

const obj = {
  value: 42,
  method: () => {
    console.log(this.value); // `this` is not obj; usually undefined
  },
};

obj.method();

// vs
const obj2 = {
  value: 42,
  method() {
    console.log(this.value);
  },
};

obj2.method(); // 42

Arguments for arrow functions:

  • Shorter syntax
  • No need to bind(this) for callbacks

Arguments for normal functions:

  • Necessary for object methods using this
  • Better for constructors (cannot use arrow functions as constructors)

Recommendation: Use arrow functions for small callbacks and when you want lexical this. Use method shorthand or function declarations for object methods or when you need a proper this or new-constructable behavior. MDN on arrow functions: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions


5) Classes vs prototypes - Is classical OOP “better” in JS?

Question: Use ES6 class syntax or stick with prototypal patterns?

Why it’s controversial: class offers familiar OOP syntax but is mostly syntactic sugar over prototypes. Purists argue you should embrace prototypes; others prefer class for readability.

Example:

// Class syntax
class Animal {
  constructor(name) {
    this.name = name;
  }
  speak() {
    console.log(this.name + ' makes a noise');
  }
}

// Prototype equivalent
function AnimalP(name) {
  this.name = name;
}
AnimalP.prototype.speak = function () {
  console.log(this.name + ' makes a noise');
};

Arguments for class:

  • More familiar to developers from other languages
  • Cleaner, less boilerplate

Arguments for prototypes:

  • Exposes the language’s original model
  • Sometimes more flexible for meta-programming

Recommendation: Prefer class syntax for clarity and maintainability unless you need low-level prototype manipulation. Classes are well-supported and widely understood.


6) Mutability vs Immutability - Should you avoid mutation at all costs?

Question: Is immutability always better, particularly in UI frameworks like React?

Why it’s controversial: Immutable patterns avoid shared-state bugs and make change detection easier, but copying data can be less performant and more verbose.

Example (React):

// Mutating state (bad)
state.items.push(newItem);
this.setState({ items: state.items });

// Immutable (good)
this.setState({ items: [...this.state.items, newItem] });

Arguments for immutability:

  • Easier reasoning about state changes
  • Simpler to implement time-travel/debugging
  • Avoids subtle shared-state bugs

Arguments for mutation:

  • More performant in some hot loops
  • Simpler in small scripts or performance-critical code when done safely

Recommendation: Prefer immutability in application state (especially UI), and use mutation carefully in performance-sensitive lower-level code with clear boundaries. Libraries like Immer help balance ergonomics and immutability: https://immerjs.github.io/immer/

React docs on immutability and state updates: https://reactjs.org/docs/state-and-lifecycle.html


7) TypeScript - Salvation or Overhead? (and the any problem)

Question: Adopt TypeScript for type safety or stick with plain JavaScript?

Why it’s controversial: TypeScript adds a compile step and type system that many find increases productivity and reliability. Critics say it adds barrier-to-entry, friction for small projects, or complexity with bad typings, especially when developers overuse any.

Arguments for TypeScript:

  • Catches many errors at compile time
  • Excellent editor tooling and refactoring
  • Better documentation via types

Arguments against TypeScript:

  • Initial learning curve and build complexity
  • Overly permissive use of any undermines benefits

Recommendation: For medium+ codebases or teams, TypeScript’s benefits usually outweigh the costs. Avoid treating any as a shortcut - prefer unknown or gradual typing and add types incrementally. Official TypeScript site: https://www.typescriptlang.org/


8) Using eval and dynamic code execution - Is it ever OK?

Question: Use eval, new Function, or string-based dynamic code?

Why it’s controversial: Powerful but risky. eval runs arbitrary code so it’s a security and performance nightmare unless tightly controlled.

Arguments against eval:

  • Security vulnerabilities (code injection)
  • Hard to debug and optimize
  • Most cases can be solved without it

Possible use cases:

  • Controlled meta-programming where inputs are trusted and performance/security considered
  • Sandboxed environments with strict controls (rare)

Recommendation: Avoid eval and new Function unless you have compelling, audited reasons. See MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval


9) Promises vs async/await - Is one superior?

Question: Prefer .then() chains or async/await syntax?

Why it’s controversial: async/await reads like synchronous code and simplifies flow, but .then() can be more explicit for some concurrent patterns and functional chaining.

Examples:

// Promise chain
fetch(url)
  .then(r => r.json())
  .then(data => doSomething(data))
  .catch(err => console.error(err));

// async/await
async function load() {
  try {
    const r = await fetch(url);
    const data = await r.json();
    doSomething(data);
  } catch (err) {
    console.error(err);
  }
}

Arguments for async/await:

  • Cleaner, imperative control flow
  • Easier to follow try/catch for errors

Arguments for promises:

  • Composability with Promise.all, race, and functional patterns
  • Non-blocking style that’s explicit about concurrency

Recommendation: Use async/await for readability in sequential code; use Promise.all/Promise.race or functional promise methods for explicit concurrency. Understand the event loop (microtasks vs macrotasks) to avoid subtle ordering issues: https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop


10) Modifying built-ins and monkey-patching - Dangerous convenience?

Question: Add methods to Array.prototype or other built-ins for convenience?

Why it’s controversial: Monkey-patching provides nice DSLs but breaks encapsulation, risks conflicts, and can surprise other libraries.

Example brokenness:

// Suppose some code adds a function
Array.prototype.flatten = function () {
  /* ... */
};

// Later, a library assumes standard Array methods and gets unexpected behavior

Arguments for monkey-patching:

  • Enables polyfills or syntactic sugar in controlled environments

Arguments against:

  • Potential for collisions and fragile code
  • Hard to reason about in a global ecosystem

Recommendation: Avoid altering global prototypes. Use polyfills via vetted libraries or write small helper functions. If you must polyfill, prefer standard-compliant polyfills that check for existing behavior.


11) Private class fields (#) vs closures/TypeScript private

Question: Use native private fields (#x) or rely on closure-based privacy or TypeScript’s private modifier?

Why it’s controversial: Native private fields are enforced at runtime and not accessible externally, but their syntax and semantics differ from TypeScript’s compile-time private.

Example:

class C {
  #secret = 42;
  getSecret() {
    return this.#secret;
  }
}

const c = new C();
// c.#secret // SyntaxError

Arguments for native private fields:

  • True privacy enforced by the runtime
  • No reliance on build-time checks

Arguments for TypeScript private:

  • Familiar syntax and integrates with type system
  • Only enforces at compile time (can be bypassed in runtime)

Recommendation: Use native private fields when you need runtime-enforced privacy. Use TypeScript private for ergonomics across codebases if you already use TypeScript, but understand it’s only a compile-time guarantee. MDN on private fields: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Private_class_fields


12) Proxies - Miracle wrapper or footgun?

Question: Use Proxy for meta-programming and reactivity (Vue, etc.) or avoid it?

Why it’s controversial: Proxy enables powerful patterns (interception, reactivity) but can complicate debugging and produce surprising performance characteristics.

Example:

const p = new Proxy(
  {},
  {
    get(target, prop) {
      console.log('get', prop);
      return target[prop];
    },
  }
);

p.x = 10;
console.log(p.x); // logs get x

Arguments for Proxy:

  • Enables frameworks to implement reactivity without modifying objects
  • Allows dynamic behavior and validation

Arguments against:

  • Harder to inspect with devtools sometimes
  • Potential performance overhead in tight loops

Recommendation: Use Proxy where it simplifies architecture (e.g., for reactive systems), but document behavior and measure performance when used in hot paths. MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy


13) Frameworks vs Vanilla JS - How much library is too much?

Question: Build with a framework (React/Vue/Angular) or use vanilla JS (or micro-libraries)?

Why it’s controversial: Frameworks accelerate development and solve complex UI problems, but they add load, lock-in, and cognitive overhead. Vanilla JS is lighter but you’ll reimplement framework features yourself.

Considerations:

  • Project complexity and team skill set
  • Long-term maintenance and upgrade paths
  • Performance and bundle size constraints

Recommendation: Choose based on needs. For complex interactive UIs, frameworks are usually justified. For small widgets, consider vanilla or micro-libraries. Always evaluate cost of maintenance and developer familiarity.


14) Tooling and style: Prettier/ESLint vs personal style

Question: Let automatic formatters and opinionated linters enforce style, or keep personal preferences?

Why it’s controversial: Developers can be attached to style choices (tabs vs spaces, semicolons, trailing commas). Tooling removes bikeshedding and maintains consistency, but some feel it’s a loss of control.

Recommendation: Use automated formatting (Prettier) and a shared linting configuration (ESLint) to reduce friction during code review. Decide on project-wide rules and commit them to version control. This reduces subjective debates and frees reviews to focus on logic.

Prettier: https://prettier.io/ ESLint: https://eslint.org/


15) Guardrails for controversial choices - Pragmatic team strategies

  • Adopt a style guide and automate it (Prettier + ESLint + CI). This removes low-signal debates.
  • Prefer clarity and explicitness in shared code; optimize later with measurements.
  • Use type systems (TypeScript) for medium+ codebases, and avoid any as a default.
  • Write tests that encode expectations around corner-case behaviors (e.g., equality, null handling).
  • Keep controversial code isolated and documented (e.g., mutation hotspots, performance-critical loops).
  • Encourage regular knowledge-sharing: code reviews, brown-bags, and RFC processes for architectural changes.

Final thoughts - Trade-offs over dogma

Most JavaScript controversies come down to trade-offs between safety, ergonomics, performance, and historical baggage. There are few absolute right answers. The healthier approach is to:

  • Understand the technical trade-offs (not just the aesthetics)
  • Make team-level decisions and encode them in tooling
  • Measure performance/security when claims are made
  • Prefer clarity for future readers of your code

If you walk away with one rule: be explicit about intent. When intent is explicit, many controversies dissolve into well-reasoned design choices.

Further reading and references

Back to Blog

Related Posts

View All Posts »
To Semicolon or Not to Semicolon: The Great JavaScript Debate

To Semicolon or Not to Semicolon: The Great JavaScript Debate

Explore the semicolon debate in JavaScript: how Automatic Semicolon Insertion (ASI) works, real-world pitfalls, arguments for both sides, what influential developers and major style guides recommend, and a practical team decision flow to pick the right approach for your project.

Understanding the Event Loop: Myths vs. Reality

Understanding the Event Loop: Myths vs. Reality

Cut through the noise: learn what the JavaScript event loop actually does, why common claims (like “setTimeout(0) runs before promises”) are wrong, how browsers and Node differ, and how to reason reliably about async code.