JavaScript is not random, but it can look random when you only see the output. Many surprising behaviors come from a few deep rules: coercion, scope, references, and the event loop.
Here are five behaviors worth understanding instead of memorizing.
Quick answer
Most JavaScript surprises come from one of these questions:
| Surprise | Rule underneath |
|---|---|
Weird + results |
Values are converted before adding or joining |
typeof null |
A historical language bug that stayed for compatibility |
| Loop callbacks show the wrong number | var is function-scoped |
this changes |
this depends on the call site |
| Promises run before timers | Microtasks run before the next task |
When you know the rule, the output becomes less mysterious.
1. [] + [] returns an empty string
Try this:
[] + []
The result is:
""
The + operator either adds numbers or concatenates strings. Arrays are converted to primitives first. An empty array becomes an empty string, so the expression becomes:
"" + ""
That is why the result is an empty string.
Now this:
[] + {}
often becomes:
"[object Object]"
The array converts to "", the object converts to "[object Object]", and string concatenation wins.
The lesson: avoid relying on implicit coercion in application code. Use explicit conversion when the value matters.
2. typeof null is object
This one is famous:
typeof null
returns:
"object"
This is a legacy behavior in JavaScript. It does not mean null is a normal object.
Check for null directly:
if (value === null) {
// handle null
}
For object checks, remember to exclude null:
function isObject(value) {
return typeof value === "object" && value !== null;
}
3. var in loops closes over one binding
This code surprises people:
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
It prints:
3
3
3
var is function-scoped, so every callback closes over the same i. By the time the callbacks run, the loop is done and i is 3.
Use let:
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
Now each iteration gets its own binding, so it prints:
0
1
2
4. this depends on how a function is called
this is not where a function is written. It is usually determined by how the function is called.
const user = {
name: "Aarav",
sayName() {
console.log(this.name);
}
};
user.sayName();
This prints "Aarav" because the function is called as a method on user.
But:
const sayName = user.sayName;
sayName();
Now this is no longer user. The function was detached from the object.
When passing methods as callbacks, bind them or use an arrow wrapper:
button.addEventListener("click", () => user.sayName());
5. Promise callbacks run before setTimeout
This code:
setTimeout(() => console.log("timeout"), 0);
Promise.resolve().then(() => console.log("promise"));
console.log("sync");
prints:
sync
promise
timeout
Synchronous code runs first. Promise callbacks go into the microtask queue. setTimeout callbacks go into the task queue. After the current stack finishes, JavaScript drains microtasks before running the next task.
This matters when you debug UI updates, background jobs, and async ordering bugs.
The pattern underneath the weirdness
Most JavaScript surprises become less surprising when you ask:
- Did a value get converted?
- Is this a primitive or reference?
- What scope does this variable live in?
- How was the function called?
- Is this callback a microtask or a task?
JavaScript has sharp edges, but the edges have rules. Learn the rules and the language starts feeling much less spooky.
Discussion
What would you try, change, or challenge after reading this guide? Specific results and errors help the next reader.
Comments will load as you reach this section.