
Exception Handling
Doing chemistry(try), Fire!(catch), Cleanup(finally).

Doing chemistry(try), Fire!(catch), Cleanup(finally).
Why does my server crash? OS's desperate struggle to manage limited memory. War against Fragmentation.

Two ways to escape a maze. Spread out wide (BFS) or dig deep (DFS)? Who finds the shortest path?

Fast by name. Partitioning around a Pivot. Why is it the standard library choice despite O(N²) worst case?

Establishing TCP connection is expensive. Reuse it for multiple requests.

Three days after launching my first side project, I got a Slack notification at 3 AM. The server was down. I frantically opened the logs and found just one line: TypeError: Cannot read property 'id' of undefined, followed by the process termination message. A user tried to upload a profile image, the file size validation logic threw an error, and because no one caught that error, the entire server crashed.
That's when I realized: errors are inevitable. The question isn't whether errors will happen, but whether your program dies or handles them gracefully. Exception handling wasn't optional—it was mandatory. This was one of the truths I, as a non-CS major, learned the hard way.
When I first learned to code, I only thought about the "happy path." I assumed users always entered valid input, APIs always returned 200, and files always existed. Reality was different.
I spent more time debugging "why isn't this working?" than actually writing code. That's when I understood: the difference between good and bad developers is how well they anticipate and prepare for errors.
When I first learned try-catch, I thought it was just "catch errors when they happen." But using it in production, I realized it wasn't just an error handling tool—it was a control flow structure.
This analogy clicked for me: try-catch is like the safety net under a circus trapeze.
Whether the show goes perfectly or someone falls, the stage must be cleaned. That's what finally does.
Working with JavaScript, all errors looked the same at first. Red text appeared and I'd just think "error happened." But errors have types.
// Missing closing quote
const message = "Hello;
// SyntaxError: Invalid or unexpected token
This blows up during the parsing phase, before execution even starts. You can't even catch it with try-catch because the code isn't valid JavaScript.
const user = null;
console.log(user.name); // TypeError: Cannot read property 'name' of null
The error I encountered most often. "I thought this was an object but it's null." Happens constantly when API responses differ from expectations.
console.log(notDefined); // ReferenceError: notDefined is not defined
Typos in variable names, or trying to access variables outside their scope.
const arr = new Array(-1); // RangeError: Invalid array length
Negative array lengths, or stack overflow from too-deep recursion.
I understood these as "bombs that explode during execution." The code is syntactically valid, but something goes wrong at runtime. And these bombs can be caught with try-catch.
I moved from JavaScript/Node.js to Java Spring at work. Java's "Checked Exception" concept stunned me.
// This won't compile!
public void readFile(String path) {
FileReader reader = new FileReader(path); // Compile Error!
// Unhandled exception: java.io.FileNotFoundException
}
// Either use try-catch
public void readFile(String path) {
try {
FileReader reader = new FileReader(path);
} catch (FileNotFoundException e) {
System.out.println("File not found: " + e.getMessage());
}
}
// Or delegate with throws
public void readFile(String path) throws FileNotFoundException {
FileReader reader = new FileReader(path);
}
Java forces you: "This method can throw FileNotFoundException, so you must handle it." JavaScript just crashes if something goes wrong, but Java requires exception handling at compile time.
Initially annoying. "Why are you forcing this on me?" But as the project grew, I understood. Every point where an exception can occur is explicitly documented in the code, so during maintenance I don't have to wonder "where could errors happen?"
// This compiles without handling
int result = 10 / 0; // ArithmeticException (subclass of RuntimeException)
RuntimeException and its subclasses (NullPointerException, ArrayIndexOutOfBoundsException, etc.) aren't checked. Why? Because they represent programmer mistakes. Don't catch these with try-catch—fix your code.
I organized it like this:
As projects grew, generic "Error" wasn't enough. When API requests failed, I needed to know if it was a network issue, authentication failure, or server error.
// Before: Everything is just Error
throw new Error("API request failed");
// After: Granular errors
class NetworkError extends Error {
constructor(message) {
super(message);
this.name = "NetworkError";
this.statusCode = 0; // Network itself is down
}
}
class AuthenticationError extends Error {
constructor(message) {
super(message);
this.name = "AuthenticationError";
this.statusCode = 401;
}
}
class ServerError extends Error {
constructor(message, statusCode) {
super(message);
this.name = "ServerError";
this.statusCode = statusCode;
}
}
This allowed different handling for different error types in catch blocks.
try {
await api.fetchUserProfile();
} catch (error) {
if (error instanceof NetworkError) {
showToast("Please check your internet connection");
} else if (error instanceof AuthenticationError) {
redirectToLogin();
} else if (error instanceof ServerError) {
logToSentry(error);
showToast("Server error. Please try again later");
} else {
console.error("Unknown error:", error);
showToast("An unknown error occurred");
}
}
I thought of this as "giving errors identification cards." Errors introduce themselves, so I can respond appropriately.
Initially I thought "do I need try-catch everywhere?" No. Errors propagate up the call stack.
function validateEmail(email) {
if (!email.includes("@")) {
throw new Error("Invalid email format");
}
}
function processUserInput(formData) {
validateEmail(formData.email); // If error happens here
// Code below doesn't execute
saveToDatabase(formData);
}
function handleSubmit() {
try {
processUserInput({ email: "invalid" }); // Error propagates here
} catch (error) {
console.error("Input processing failed:", error.message);
}
}
When validateEmail throws an error, it skips processUserInput and goes straight to handleSubmit's catch block. Like when a fire alarm goes off, you skip all floors and go straight to the ground floor exit.
After understanding this, I started throwing errors in business logic and catching only at the controller or top level.
When first learning Promises, error handling was most confusing.
// This doesn't catch the error!
try {
fetch("https://api.example.com/data")
.then(res => res.json())
.then(data => console.log(data));
} catch (error) {
// Never reaches here!
console.error(error);
}
Promises execute asynchronously, so try-catch doesn't work. By the time fetch executes, we've already exited the try block.
fetch("https://api.example.com/data")
.then(res => {
if (!res.ok) {
throw new Error(`HTTP error! status: ${res.status}`);
}
return res.json();
})
.then(data => console.log(data))
.catch(error => {
// Catches errors here
console.error("API request failed:", error);
})
.finally(() => {
// Runs whether success or failure
hideLoadingSpinner();
});
async function fetchUserData() {
try {
const response = await fetch("https://api.example.com/user");
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error("Failed to load user data:", error);
return null; // Return default value
} finally {
hideLoadingSpinner();
}
}
Using async/await, I felt "finally, I can handle errors in async code like sync code." The code became much more readable.
No matter how well you use try-catch, some errors slip through. So I set up a final safety net with global error handlers.
// Synchronous errors
process.on("uncaughtException", (error) => {
console.error("Uncaught exception:", error);
logErrorToFile(error);
// Process state may be unstable
process.exit(1);
});
// Promise rejections
process.on("unhandledRejection", (reason, promise) => {
console.error("Unhandled promise rejection:", reason);
logErrorToSentry(reason);
});
window.addEventListener("error", (event) => {
console.error("Global error:", event.error);
Sentry.captureException(event.error);
});
window.addEventListener("unhandledrejection", (event) => {
console.error("Unhandled promise error:", event.reason);
Sentry.captureException(event.reason);
});
But global handlers are the last resort. Errors reaching here mean program state might be unstable. All you can do is log and gracefully shut down.
If you just "console.log and done," you have nothing when debugging later. I established these principles:
try {
await updateUserProfile(userId, newData);
} catch (error) {
// Bad: Only log the error
console.error(error);
// Good: Include context
console.error("User profile update failed", {
userId,
attemptedData: newData,
error: error.message,
stack: error.stack,
timestamp: new Date().toISOString()
});
}
// Warning level (recoverable)
logger.warn("Cache refresh failed, using default", { error });
// Error level (feature broken)
logger.error("Payment processing failed", { error, orderId });
// Fatal level (entire service down)
logger.fatal("Database connection unavailable", { error });
// Bad: Password ends up in logs
console.error("Login failed", { email, password });
// Good: Exclude sensitive info
console.error("Login failed", { email, errorCode: error.code });
The most common pattern I use in production:
async function fetchWithErrorHandling(url, options = {}) {
try {
const response = await fetch(url, options);
// Check HTTP status code
if (!response.ok) {
// 4xx: Client error
if (response.status >= 400 && response.status < 500) {
const errorData = await response.json();
throw new AuthenticationError(errorData.message);
}
// 5xx: Server error
if (response.status >= 500) {
throw new ServerError(`Server error (${response.status})`);
}
}
const data = await response.json();
return { success: true, data };
} catch (error) {
// Network itself is down
if (error instanceof TypeError && error.message.includes("fetch")) {
return {
success: false,
error: new NetworkError("Please check your internet connection")
};
}
// Our custom errors pass through
if (error instanceof NetworkError ||
error instanceof AuthenticationError ||
error instanceof ServerError) {
return { success: false, error };
}
// Unexpected error
console.error("Unexpected error:", error);
return {
success: false,
error: new Error("An unknown error occurred")
};
}
}
// Usage
const result = await fetchWithErrorHandling("/api/user/profile");
if (result.success) {
displayProfile(result.data);
} else {
showErrorMessage(result.error.message);
}
class ValidationError extends Error {
constructor(field, message) {
super(message);
this.name = "ValidationError";
this.field = field;
}
}
function validateLoginForm(formData) {
if (!formData.email) {
throw new ValidationError("email", "Please enter your email");
}
if (!formData.email.includes("@")) {
throw new ValidationError("email", "Invalid email format");
}
if (!formData.password) {
throw new ValidationError("password", "Please enter your password");
}
if (formData.password.length < 8) {
throw new ValidationError("password", "Password must be at least 8 characters");
}
}
function handleLogin(formData) {
try {
validateLoginForm(formData);
await login(formData);
} catch (error) {
if (error instanceof ValidationError) {
showFieldError(error.field, error.message);
} else {
showToast("Login failed");
}
}
}
async function connectDatabase(retries = 3) {
for (let attempt = 1; attempt <= retries; attempt++) {
try {
const db = await mongoose.connect(process.env.DB_URL);
console.log("DB connection successful");
return db;
} catch (error) {
console.error(`DB connection failed (attempt ${attempt}/${retries})`, error);
if (attempt === retries) {
throw new Error("Failed to connect to database");
}
// Wait before retry (exponential backoff)
const waitTime = Math.pow(2, attempt) * 1000;
await new Promise(resolve => setTimeout(resolve, waitTime));
}
}
}
// Bad: You don't even know an error occurred
try {
riskyOperation();
} catch (error) {
// Do nothing
}
// Good: At least log it
try {
riskyOperation();
} catch (error) {
console.error("riskyOperation failed:", error);
}
// Bad: All errors handled identically
try {
await api.call();
} catch (error) {
showToast("An error occurred");
}
// Good: Handle by error type
try {
await api.call();
} catch (error) {
if (error.status === 401) {
redirectToLogin();
} else if (error.status === 500) {
showToast("Server error");
logToSentry(error);
} else {
showToast("Request failed: " + error.message);
}
}
// Bad: Hiding programmer mistakes
try {
const result = someUndefinedVariable.property;
} catch (error) {
// Should fix the code, but hiding it instead
}
// Good: Let these crash during development so you find them
Exception handling isn't just "catch errors when they happen." It's about maintaining control when your program faces unpredictable situations. When writing code now, I always think:
Try is attempting with hope, catch is acknowledging failure and responding, finally is taking responsibility regardless of outcome. These three together create stable software.
Since that night my server died at 3 AM, I add exception handling to all critical logic. Errors are unavoidable, but disasters from errors are preventable. That's the essence of exception handling I've embraced.