Control Flow & Error Handling
Advanced Patterns & Edge Cases
Block Scoping: var vs let/const
var declarations are function-scoped, not block-scoped:
var x = 1;
{
var x = 2;
}
console.log(x); // 2 (var persists beyond block)
let y = 1;
{
let y = 2;
}
console.log(y); // 1 (truly block-scoped)
Falsy Values & Object Wrappers
Falsy values: false, undefined, null, 0, NaN, "" (empty string)
Subtle trap: Boolean object wrappers are truthy, unlike primitives:
const b = new Boolean(false);
if (b) {
console.log("This executes - b is an object (truthy)");
}
if (b == true) {
console.log("This does NOT execute");
}
Switch Fall-through
Omitting break causes execution to continue to the next case:
switch (fruitType) {
case "Bananas":
console.log("Bananas are $0.48 a pound.");
break; // Required to prevent fall-through
case "Cherries":
console.log("Cherries are $3.00 a pound.");
break;
}
Pattern: Intentional fall-through works for multiple matching cases, but must be explicitly documented:
switch (status) {
case "pending":
case "loading":
return "In progress...";
case "completed":
return "Done!";
}
Error Handling Nuances
Finally Block Overrides return and throw
A return in the finally block overwrites any return value or exception from try/catch:
function f() {
try {
console.log(0);
throw "error";
} catch (e) {
console.log(1);
return true; // Suspended until finally runs
} finally {
console.log(3);
return false; // OVERWRITES the catch return
}
}
console.log(f()); // Logs: 0, 1, 3, false
Finally Can Suppress Exceptions
A return in finally can prevent thrown exceptions from propagating:
function f() {
try {
throw "error";
} catch (e) {
console.log("caught");
throw e; // Suspended until finally runs
} finally {
return false; // This suppresses the throw - exception never propagates
}
}
try {
console.log(f()); // Logs: "caught", false
} catch (e) {
// Never executes - finally prevented the throw
}
Nested try...catch Requirements
When an inner try block has no catch:
try {
try {
throw new Error("inner");
} finally {
// This try MUST have finally if no catch
}
} catch (e) {
// Catches from inner try's finally
}
Error Objects: Discrimination Patterns
Use the Error constructor to enable consistent name and message properties for discriminating between exception types:
function doSomethingErrorProne() {
if (ourCodeMakesAMistake()) {
throw new Error("The message");
}
}
try {
doSomethingErrorProne();
} catch (e) {
console.error(e.name); // 'Error', 'TypeError', 'RangeError', etc.
console.error(e.message); // Specific message
// Discriminate by type
if (e instanceof TypeError) {
// Handle type errors
} else if (e instanceof RangeError) {
// Handle range errors
}
}
Exception Hierarchies
JavaScript has multiple exception types:
- ECMAScript exceptions: RangeError, TypeError, ReferenceError, SyntaxError, EvalError, URIError
- DOMException: Web API specific exceptions
Catch blocks can handle different types differently:
try {
// Code that might throw different error types
} catch (e) {
if (e instanceof DOMException) {
// Handle DOM-related errors
} else if (e instanceof ReferenceError) {
// Handle reference errors
}
}
Debugging with console.error()
Use console.error() instead of console.log() in catch blocks:
catch (err) {
console.error(err); // Formats as error, tracks in error list
// console.log(err) is not recommended for debugging
}