DEV Community

ValPetal Tech Labs
ValPetal Tech Labs

Posted on

Question of the Day #22 [Talk::Overflow]

Why a function declaration after a return statement still shadows your outer variable — and how this "dead code" hoisting trap silently breaks state updates in production JavaScript.

This post explains a quiz originally shared as a LinkedIn poll.


🔹 The Question

var x = 1;

function bar() {
  x = 10;
  console.log(x);
  return;
  function x() {}
}

bar();
console.log(x);
Enter fullscreen mode Exit fullscreen mode

Hint: Before any code inside a function runs, the engine has already processed all declarations in the function body — including ones that appear after a return statement.

Follow me for JavaScript puzzles and weekly curations of developer talks & insights at Talk::Overflow: https://talkoverflow.substack.com/


🔹 Solution

Correct answer: B) 10, 1

The first console.log prints 10. The second prints 1.

🧠 How this works

This quiz exploits the fact that function declarations are hoisted to the top of their enclosing function scope — even when they appear after a return statement, where they can never be reached at runtime.

When the engine enters bar(), it performs declaration hoisting before executing any code. It finds function x() {} at the bottom of the function body and creates a local binding x initialized to that function. This local x completely shadows the outer var x = 1.

At runtime, the function effectively becomes:

function bar() {
  var x = function x() {};  // hoisted function declaration
  x = 10;                   // reassigns local x to 10
  console.log(x);           // 10
  return;
}
Enter fullscreen mode Exit fullscreen mode

The x = 10 assignment targets the local x, not the outer one. The outer x remains 1 throughout.

This pattern commonly surfaces when developers add utility functions inside other functions during refactoring, not realizing those function names may collide with outer variables.

🔍 Line-by-line explanation

  1. var x = 1; — Declares x in the outer (global/module) scope and initializes it to 1.

  2. function bar() { — The engine parses the entire body of bar before executing it. It encounters function x() {} and hoists it — creating a local binding x inside bar, initialized to the function object. This local x shadows the outer x for the entire duration of bar.

  3. x = 10; — This assignment targets the local x (the hoisted function declaration). The local x is now 10. The outer x is untouched.

  4. console.log(x); — Prints 10 (the local x).

  5. return; — Exits bar. Everything below this line is unreachable at runtime.

  6. function x() {} — This is a function declaration. Its positional placement after return is irrelevant — the declaration was already hoisted during the parsing phase. It has no runtime effect here.

  7. bar(); — Calls bar, which logs 10 as described above.

  8. console.log(x); — Prints the outer x, which is still 1. The assignment inside bar never touched it.

The non-obvious part: Most developers read x = 10 and assume it refers to the outer x because they don't see any local declaration of x before the return. The function x() {} after return looks dead — but its declaration is very much alive due to hoisting.


🔹 Key Takeaways

  1. Function declarations are hoisted regardless of reachability. A function declaration after return, inside a never-entered if branch, or at the end of a thousand-line function is still hoisted to the top of its enclosing scope.

  2. Hoisted function declarations create local bindings that shadow outer variables. Unlike a simple x = 10 assignment (which would target the outer scope), the presence of function x() {} anywhere in the function body creates a local x that captures all references to x within that function.

  3. "Dead code" can have live side effects. In JavaScript, unreachable function declarations still participate in scope creation. Never assume code after return is safe to ignore during code review.

  4. Use const/let and arrow functions to avoid this class of bugs. const x = () => {} is not hoisted and will throw a ReferenceError if accessed before its declaration, making the bug immediately visible instead of silent.

  5. Linting rules like no-unreachable and no-shadow catch this pattern. Enable them to flag function declarations after return statements and variable name collisions across scopes.

Top comments (0)