Skip to main content

Command Palette

Search for a command to run...

Error Handling & Modules in JavaScript

Handling JavaScript Errors and Organizing Code Using Modules

Published
9 min read
Error Handling & Modules in JavaScript
S

I'm a passionate backend dev

Hey, JavaScript rockstars! Ever had your app crash and burn because of a sneaky bug, or found yourself drowning in a jalebi ➿ mess of code? Yeah, we’ve all been there. Today, we’re tackling two game-changers: error handling with try-catch and custom errors, and modules with import/export. By the end, you’ll be dodging disasters like a ninja and organizing your code like a boss.

Grab your favorite drink, and let’s dive into this JavaScript party!

Handling Errors: From Chaos to Control

Errors in JavaScript are like uninvited guests—they show up, ruin the vibe, and leave you scrambling. But with try, catch, and a sprinkle of custom error magic, you can kick them out and keep the party going.

Try-Catch: The Safety Net

try and catch are your trusty bouncers. Wrap risky code in try, and if it flops, catch swoops in to save the day. Here’s the basics:

function divide(a, b) {
  try { // Start of the 'try' block - code that might throw an error goes here
    if (b === 0) { // Check if the divisor 'b' is zero
      throw new Error("Division by zero? Nope!"); // If b is zero, explicitly throw a new Error object.
      // The message will be used to describe the error.
    }
    return a / b; // If b is not zero, perform the division and return the result.
  } catch (error) { // Start of the 'catch' block - this block executes ONLY if an error is thrown in the 'try' block
    console.log("Oops! " + error.message); // If an error is caught, log a message to the console.
    // 'error' is the Error object that was thrown, and 'error.message' contains the error message.
  }
}

divide(10, 2); // Call divide with valid inputs - Output: 5 (return value, not logged)
divide(10, 0); // Call divide with invalid input (division by zero) - Output: Oops! Division by zero? Nope! (logged from catch)

See that? try tests the waters, and catch catches the mess. No more crashing—just a polite “Oops!”.

Adding finally for the Win

Sometimes you want to run cleanup code, win or lose. That’s where finally steps in:

function riskyBusiness() {
  try { // Start of the 'try' block
    console.log("Trying something crazy..."); // Log a message before potentially risky operation
    throw new Error("Boom!"); // Intentionally throw an error to simulate a problem
  } catch (error) { // Start of the 'catch' block - will catch the error thrown in 'try'
    console.log("Caught it: " + error.message); // Log the caught error message
  } finally { // Start of the 'finally' block - this ALWAYS executes, regardless of try or catch outcome
    console.log("Cleaning up, no matter what!"); // Log a message indicating cleanup actions.
    // 'finally' is used for code that MUST run (e.g., closing files, releasing resources).
  }
}

riskyBusiness();
// Output:
// Trying something crazy...
// Caught it: Boom!
// Cleaning up, no matter what!

finally runs whether the try succeeds or flops—perfect for closing files, resetting states, or just flexing your control.

Custom Errors: Make It Personal

Why settle for generic errors when you can craft your own? Extend the Error class and throw something with personality:

class PartyError extends Error { // Define a custom error class 'PartyError' that inherits from the built-in 'Error' class
  constructor(message) { // Constructor for the PartyError class, takes an error message as input
    super(message); // Call the constructor of the parent class 'Error' with the provided message.
    // This sets the 'message' property of the Error object.
    this.name = "PartyError"; // Set the 'name' property of the error to "PartyError" to identify the error type.
  }
}

function joinParty(energy) {
  try { // Start of 'try' block
    if (energy < 50) { // Check if 'energy' level is below the threshold
      throw new PartyError("You’re too tired for this party!"); // If energy is too low, throw a custom 'PartyError'
    }
    console.log("Welcome to the dance floor!"); // If energy is sufficient, welcome to the party
  } catch (error) { // Start of 'catch' block - will catch errors thrown in 'try'
    console.log(`${error.name}: ${error.message}`); // Log the error's name (PartyError) and message.
  }
}

joinParty(75); // Call joinParty with sufficient energy - Output: Welcome to the dance floor!
joinParty(20); // Call joinParty with insufficient energy - Output: PartyError: You’re too tired for this party!

Now your errors have flair! Use them to signal specific issues in your app—readers will love the clarity.

Real-World Use Case: Fetching data with grace:

async function fetchData(url) { // Define an asynchronous function 'fetchData' that takes a URL
  try { // Start of 'try' block
    const response = await fetch(url); // Use 'fetch' to make a network request to the given URL and wait for the response
    if (!response.ok) { // Check if the HTTP response status code indicates failure (e.g., 404, 500)
      throw new Error("Network flopped!"); // If the response is not 'ok' (status codes 200-299), throw a generic Error
    }
    const data = await response.json(); // If the response is 'ok', parse the response body as JSON and wait for it
    console.log("Data’s here:", data); // Log the fetched data to the console
  } catch (error) { // Start of 'catch' block - catches errors from 'fetch' or JSON parsing
    console.log("Fetch fail: " + error.message); // Log an error message indicating a fetch failure
  }
}

fetchData("https://api.example.com/stuff"); // Call fetchData with a URL (replace with a real API endpoint to test)

No crashes, just smooth handling. Your app’s ready for the real world!

JavaScript Modules: Organize Like a Pro

Now let’s talk modules—JavaScript’s way of keeping your code tidy and shareable. With import and export, you can split your app into neat little files and stitch them together like a pro.

Export: Sharing the Goods

Modules let you export functions, objects, or variables from one file to use in another. Here’s a file called utils.js :

// utils.js (This is a separate file that exports functions and constants)
export function sayHi(name) { // Exporting a function named 'sayHi'
  return `Hello, ${name}!`; // Function returns a greeting string
}

export const PI = 3.14159; // Exporting a constant named 'PI' with a numeric value

export class Greeter { // Exporting a class named 'Greeter'
  greet() { // Method within the Greeter class
    console.log("Hey there!"); // logs a greeting to the console
  }
}

You can also use a default export for the star of the show:

// math.js
export default function add(a, b) { // Exporting a function as the default export of this module
  return a + b; // Function adds two numbers and returns the sum
}

Import: Bringing It In

Now, in another file (say, main.js), you can grab what you need:

// main.js
import { sayHi, PI } from './utils.js'; // Named import: Import specific exports 'sayHi' and 'PI' from './utils.js'
// Curly braces are used for named imports.
import add from './math.js'; // Default import: Import the default export from './math.js' and give it the name 'add'
// No curly braces for default imports.

console.log(sayHi("Alex")); // Call the imported 'sayHi' function - Output: Hello, Alex!
console.log(PI); // Access the imported constant 'PI' - Output: 3.14159
console.log(add(2, 3)); // Call the imported 'add' function (default export) - Output: 5

Run this with a <script type="module"> tag in your HTML, or use a bundler like Vite, and you’re good.

Named vs. Default: Named exports ({ sayHi }) are explicit—you pick what you want. Default exports (add) are the one-and-only VIPs of the file.

Import All the Things

Got a ton to import? Use the * wildcard:

// main.js
import * as utils from './utils.js';

console.log(utils.sayHi("Sam")); // Access 'sayHi' function via the 'utils' namespace - Output: Hello, Sam!
console.log(utils.PI); // Access 'PI' constant via the 'utils' namespace - Output: 3.14159
new utils.Greeter().greet(); // Create a new instance of the 'Greeter' class via the 'utils' namespace and call 'greet' method - Output: Hey there!

It’s like grabbing the whole toolbox in one go.

Real-World Use Case: A mini app with error handling and modules:

// errors.js (Module defining custom error classes)
export class ApiError extends Error { // Define a custom error class 'ApiError' inheriting from 'Error'
  constructor(message) { // Constructor for ApiError, takes an error message
    super(message); // Call the parent Error class constructor with the message
    this.name = "ApiError"; // Set the 'name' property to identify this error type
  }
}
// api.js
import { ApiError } from './errors.js'; // Import the custom 'ApiError' class from './errors.js'

export async function getUser(id) { // Export an async function 'getUser' to fetch user data by ID
  try { // Start of 'try' block
    const response = await fetch(`https://api.example.com/users/${id}`); // Fetch user data from an API endpoint
    if (!response.ok) { // Check if the response status is not successful
      throw new ApiError("User fetch failed!"); // Throw a custom 'ApiError' if the fetch fails
    }
    return await response.json(); // If successful, parse the JSON response and return it
  } catch (error) { // Start of 'catch' block - catches errors from 'fetch' or JSON parsing, or ApiError
    console.log(`${error.name}: ${error.message}`); // Log the error's name and message to the console
// This handles potential network errors or API specific errors.
  }
}

// main.js
import { getUser } from './api.js'; // Import the 'getUser' function from './api.js'

getUser(1); // Call 'getUser' to fetch user with ID 1
// This will either fetch and (likely) return user data (though data is not used here),
// or it will log "ApiError: User fetch failed!" to the console if there's an error during fetching.

Look at that—clean, modular, and error-proof. Your codebase just got a glow-up!


Putting It Together: Errors + Modules

Let’s combine these superpowers into one slick example—a button that fetches data:

// fetcher.js
export class FetchError extends Error { // Define custom error class 'FetchError'
  constructor(message) { // Constructor for FetchError
    super(message); // Call parent Error constructor
    this.name = "FetchError"; // Set error name
  }
}

export async function fetchWithStyle(url) { // Export async function for styled fetch
  try { // Try block
    const response = await fetch(url); // Fetch data
    if (!response.ok) { // Check if response is not ok
      throw new FetchError("Bad vibes from the server!"); // Throw custom FetchError for bad response
    }
    return await response.json(); // Parse JSON if response is ok
  } catch (error) { // Catch block
    throw error; // Re-throw the error - let the caller handle it. This is different from previous examples
    // where errors were logged within the function. Here, the function is designed to *report* the error upwards.
  }
}

// app.js
import { fetchWithStyle, FetchError } from './fetcher.js'; // Import fetchWithStyle function and FetchError class

const button = document.querySelector("#fetch-btn"); // Get the button element from the HTML by its ID

button.addEventListener("click", async () => { // Add an event listener to the button for 'click' events
  try { // Try block - for handling potential errors during the button click action
    const data = await fetchWithStyle("https://api.example.com/data"); // Use the styled fetch function to get data
    console.log("Success:", data); // Log success and the fetched data if no error
  } catch (error) { // Catch block - to handle errors that might occur during fetchWithStyle
    console.log(`${error.name}: ${error.message}`); // Log the error name and message to the console
    // This will catch FetchError (or any other error re-thrown by fetchWithStyle).
  }
});

HTML:

<button id="fetch-btn">Get Data!</button>
<script type="module" src="app.js"></script>

Click that button, and you’ve got modular code with rock-solid error handling. Boom!

Error handling with try-catch and custom errors, plus modules with import/export, all served up with a pile of code and a dash of fun. You’re ready to catch chaos now.

Until next time! 🙌