BitlystBitlyst
PostsSeriesTopicsAbout

Bitlyst

Short, diagram-first posts on JavaScript, React & Next.js. Follow via RSS or the newsletter.

ยฉ 2026 Mohsen Fallahnejad

PostsSeriesTopicsLinksAboutRSSGitHubX
Posts/javascript
Beginner5 min read

Part 3 of JavaScript Plot Twists

The Settings Object That Came Back undefined

How automatic semicolon insertion (ASI) turns `return` followed by a newline into `return;`, and the lines where JavaScript refuses to add a semicolon at all.

Mohsen Fallahnejad

Mohsen Fallahnejad

Nov 2, 2026Discuss

Picture a settings panel that crashes the moment it opens:

code
TypeError: Cannot read properties of undefined (reading 'theme')

The line that throws is about as boring as code gets:

js
const settings = getDefaultSettings();
applyTheme(settings.theme); // ๐Ÿ’ฅ settings is undefined

And getDefaultSettings is even more boring. It has no branches, no await, no early exit. It builds an object and returns it:

js
function getDefaultSettings() {
  return
  {
    theme: "dark"
  };
}

There's an object right there. The function can't possibly return undefined.

It does, every time.


The wrong turn: blaming the caller#

When a value shows up as undefined, the natural instinct is to look upstream. Maybe the import is wrong and you're calling a different getDefaultSettings. Maybe something mutates the object after it's returned. Maybe the function is async and someone forgot an await.

Log right at the source:

js
console.log(getDefaultSettings()); // undefined

No import, no mutation, no promise. The function itself hands back nothing. The bug is somewhere in five lines, and four of them are braces.


The reveal: JavaScript added a semicolon for you#

JavaScript lets you leave out most semicolons. To make that work, the parser follows a set of rules called automatic semicolon insertion, or ASI. Most of the time ASI is invisible: it adds a semicolon where you clearly meant one.

But a few statements are treated as finished the moment a line ends. return is one of them. If nothing follows return on the same line, the parser doesn't look at the next line to see if the value is there. It ends the statement on the spot.

How the parser reads that function

  1. 1

    return โŽ

    The line ends right after return. For return, a line break counts as the end of the statement

  2. 2

    insert ;

    ASI turns it into return; โ€” a return with no value

  3. 3

    return undefined

    A bare return gives back undefined

  4. 4

    { theme: 'dark' } never runs

    The braces become a plain code block with a label called theme and the string 'dark' inside it. It sits after a return, so it's dead code

Nothing errors, because { theme: 'dark' } happens to be valid code on its own. It just isn't an object anymore.

That last step is the cruel part. With one property, the "object" is legal as a block with a label. Add a second property and the parser finally complains:

js
function getDefaultSettings() {
  return
  {
    theme: "dark",
    fontSize: 14
  };
}
// SyntaxError: Unexpected token ':'

So the bug only stays silent while the object is small, which is exactly when nobody bothers to look at it twice.

return isn't alone. The spec calls these restricted productions: places where a line break is not allowed. The ones you'll actually hit:

You writeJavaScript reads
returnโŽvaluereturn; value;
throwโŽnew Error()SyntaxError: a line break isn't allowed after throw
breakโŽlabel / continueโŽlabelbreak; label;
countโŽ++count; ++โ€ฆ (the ++ attaches to the next line)
(a)โŽ=> aSyntaxError: the arrow must be on the same line as its parameters

The mirror image: where ASI won't help#

ASI also fails in the opposite direction. It only inserts a semicolon when the next line can't continue the current one. If the next line starts with (, [ or `, it usually can, so no semicolon goes in:

js
const greeting = "hi"
[1, 2].forEach((n) => console.log(n))
// TypeError: Cannot read properties of undefined (reading 'forEach')

JavaScript reads that as "hi"[1, 2].forEach(...). The comma inside the brackets means "take the last value", so it's "hi"[2], which is undefined.

Same story with an IIFE:

js
const total = 1
(function () {})()
// TypeError: 1 is not a function

That's 1(function () {})(): calling the number 1 like a function.

ASI adds a semicolon where a line can't continue, never where you meant one.


The fix#

Start the value on the same line as return. For a multi-line object, open the brace or a parenthesis right after the keyword:

js
// before
return
{
  theme: "dark"
};

// after
return {
  theme: "dark"
};

// also fine, handy for long JSX or expressions
return (
  someLongCondition && anotherCondition
);

And let tools catch the rest:

  • Prettier reformats returnโŽ{ into return; followed by a block, which makes the bug obvious in the diff.
  • ESLint's no-unreachable rule flags the block after the bare return.
  • TypeScript marks it as "Unreachable code detected" in the editor.

If you write without semicolons, the one habit that matters is the mirror case: put a ; in front of any line that starts with (, [ or `.


The rule#

Whatever a return, throw, break or continue gives back has to start on the same line as the keyword. The parser doesn't look at the next line.


TL;DR#

  • Automatic semicolon insertion ends a return statement at the line break, so return followed by a value on the next line returns undefined.
  • A one-property object after a bare return is still valid code (a labeled block), so nothing errors. Only larger objects throw a SyntaxError.
  • The same rule applies to throw, break, continue, postfix ++ and arrow functions.
  • In the other direction, ASI doesn't add a semicolon before lines starting with (, [ or `, so those lines merge with the line above.
  • Keep the returned value on the return line, and let Prettier, ESLint's no-unreachable or TypeScript flag the rest.
Filed under#javascript#asi#semicolons#syntax#gotchas

Series

JavaScript Plot Twists

Real bugs caused by JavaScript doing exactly what the spec says โ€” and nothing you expected.

  1. 1The Leaderboard That Ranked 9 Points Above 100
  2. 2You Selected Rows 3, 8, 12, 15 and 20. JavaScript Checked 3, 1 and 8.
  3. 3The Settings Object That Came Back undefined ยท you are here
  4. 4The $4.35 Item That Was Charged $4.34
  5. 5The Leaderboard Arrived Sorted. JavaScript Re-sorted It by User ID.
  6. 6The birthday that showed up a day early ยท coming soon
  7. 7The forEach that didn't wait for await ยท coming soon
  8. 8The regex that matched every other time ยท coming soon
Next part: The $4.35 Item That Was Charged $4.34 โ†’

Was this worth your time?

One tap tells me what to write more of.

Mohsen Fallahnejad

Written by

Mohsen Fallahnejad

Frontend engineer explaining JavaScript, React & Next.js internals in posts you can finish over coffee. More about me

X โ†—GitHub โ†—LinkedIn โ†—
โ† OlderYou Selected Rows 3, 8, 12, 15 and 20. JavaScript Checked 3, 1 and 8.Newer โ†’The Server Action That Ran Twice, but Only on localhost

Start the discussion

Questions, corrections, a better approach? I read and reply to every comment.

Read next

The $4.35 Item That Was Charged $4.34

Why 0.1 + 0.2 !== 0.3 in JavaScript, how floating-point math drops a cent when converting dollars to cents, and why money should be stored as integers.

Nov 3, 20265 min read

The Leaderboard Arrived Sorted. JavaScript Re-sorted It by User ID.

Why JavaScript object key order puts integer-like keys first in ascending order, how that silently reorders JSON from your API, and when to use an array or Map.

Nov 3, 20265 min read

You Selected Rows 3, 8, 12, 15 and 20. JavaScript Checked 3, 1 and 8.

Why ['1', '7', '11'].map(parseInt) returns [1, NaN, 3]: map passes the index as parseInt's radix. Plus the Number('') trap hiding in the obvious fix.

Nov 1, 20264 min read2

Get new posts in your inbox

No spam. Unsubscribe any time.

5 min left
Discuss๐Ÿ‘ React
๐Ÿ‘