· 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 »

Using React with TypeScript: Tips and Tricks

Practical, example-driven guide to using TypeScript with React. Covers component typing, hooks, refs, generics, polymorphic components, utility types, and tooling tips to make your React code safer and more maintainable.