Skip to main content

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
}