Error Handling, Modules, Map, Find & filter and ES6 Concepts

Error Handling

Error handling in JavaScript allows you to gracefully handle unexpected situations in your code. This is important for creating robust applications that can handle errors without crashing or producing incorrect results. For handling these situations we have try, catch and finally

try...catch...finally Statement

The try...catch statement provides a way to handle exceptions that may occur during the execution of your code.

Syntax

try {
  // Code that may throw an error
} catch (error) {
  // Code to handle the error
} finally {
  // Code that will run regardless of whether an error occurred or not
}
  • The finally statement will always execute no matter if we have caught an error or not.
try {
  let result = 10 / 0; // This will not throw an error
  console.log(result);

  let person = undefined;
  console.log(person.name); // This will throw an error
} catch (error) {
  console.error("An error occurred:", error.message);
} finally {
  console.log("I will run even if there's an error.");
}

Throwing Custom Errors

You can throw your own errors using the throw statement. This is useful for creating custom error messages for specific situations.

function divide(a, b) {
  if (b === 0) {
    throw new Error("Division by zero is not allowed");
  }
  return a / b;
}

try {
  let result = divide(10, 0);
  console.log(result);
} catch (error) {
  console.error("Error:", error.message);
}

The Error Object
When an error occurs, JavaScript creates an Error object with properties such as name and message.

try {
  let result = divide(10, 0);
} catch (error) {
  console.error("Name:", error.name);     // Output: Error
  console.error("Message:", error.message); // Output: Division by zero is not allowed
}

Nested try...catch Statements

You can nest try...catch blocks to handle different errors separately.

try {
  try {
    let result = divide(10, 0);
  } catch (error) {
    console.error("Error in inner try block:", error.message);
  }

  // Some other code that may throw an error
  let person = null;
  console.log(person.name); // This will throw an error
} catch (error) {
  console.error("Error in outer try block:", error.message);
}

Custom Error Classes

For more sophisticated error handling, you can define custom error classes by extending the built-in Error class.

class ValidationError extends Error {
  constructor(message) {
    super(message);
    this.name = 'ValidationError';
  }
}

function validateAge(age) {
  if (age < 18) {
    throw new ValidationError('Age must be at least 18');
  }
  return true;
}

try {
  validateAge(15);
} catch (error) {
  if (error instanceof ValidationError) {
    console.error('Validation Error:', error.message);
  } else {
    console.error('General Error:', error.message);
  }
}
  • Error Types: Syntax errors, runtime errors, and logical errors.

  • try...catch...finally: Main structure for handling exceptions.

  • Throwing Errors: Use the throw statement to create custom errors.

  • Error Object: Contains details about the error.

  • finally Block: Executes code regardless of the error occurrence.

  • Nested try...catch: Handle different errors at different levels.

  • Custom Error Classes: Create specific error types for more granular error handling.

Proper error handling ensures your code can manage unexpected issues gracefully, providing a better user experience and making debugging easier.

Modules in JavaScript

JavaScript modules are a way to organize and structure your code by splitting it into reusable pieces. Modules allow you to encapsulate code, making it easier to manage, maintain, and reuse. They also help avoid naming conflicts by providing a way to scope variables and functions.

ES6 Modules

ES6 (ECMAScript 2015) introduced a native module system for JavaScript. ES6 modules are now widely supported in modern browsers and in Node.js.

Syntax

  • Exporting: To make functions, objects, or primitives available for import by other modules.

  • Importing: To use the exported functionalities from other modules.

Exporting

You can export values using the export keyword. There are two types of exports: named exports and default exports.

// math.js
export const PI = 3.14;

export function add(a, b) {
  return a + b;
}

Default Export

// greet.js
export default function greet(name) {
  return `Hello, ${name}!`;
}
  • There can be only one default export inside a file, more than one will throw an error.

Importing

You can import values using the import keyword.

// main.js
import { PI, add } from './math.js';

console.log(PI); // Output: 3.14
console.log(add(2, 3)); // Output: 5
  • Inside {} we declare that we want these functions from the file we are exporting from. We have this option so that our main.js file won't get bloated with functions we don't want to use, we will simply just import the functions we require, else the server will get slowed down.
Importing Default Exports
// main.js
import greet from './greet.js';

console.log(greet("Aquib")); // Output: Hello, Aquib!

Combining Named and Default Exports

A module can have both named and default exports.

// utils.js
export const square = x => x * x;
export const cube = x => x * x * x;

export default function multiply(a, b) {
  return a * b;
}
// main.js
import multiply, { square, cube } from './utils.js';

console.log(square(3)); // Output: 9
console.log(cube(3)); // Output: 27
console.log(multiply(2, 3)); // Output: 6

Renaming Imports and Exports

You can rename imports and exports to avoid naming conflicts.

Renaming Exports
// constants.js
const PI = 3.14;
const E = 2.71;

export { PI as PI_VALUE, E as EULER_NUMBER };
// main.js
import { PI_VALUE, EULER_NUMBER } from './constants.js';

console.log(PI_VALUE); // Output: 3.14
console.log(EULER_NUMBER); // Output: 2.71

Or we can do renaming Imports

// constants.js
const PI = 3.14;
const E = 2.71;

export { PI, E };
// main.js
import { PI as PI_VALUE, E as EULER_NUMBER } from './constants.js';

console.log(PI_VALUE); // Output: 3.14
console.log(EULER_NUMBER); // Output: 2.71

Both works the same and both ways are correct.

JavaScript modules help create more maintainable, scalable, and organized code by providing a way to encapsulate and reuse functionalities.

map, filter, & find

JavaScript provides several powerful array methods (Predefined built-in functions) for manipulating and iterating over arrays. Among the most commonly used are map, filter, and find. These methods help in creating new arrays based on specific criteria and transformations, making them essential tools for working with arrays in a functional programming style.

map

The map method creates a new array populated with the results of calling a provided function on every element in the calling array.

Syntax

array.map(callback(currentValue, index, array), thisArg);
  • callback: Function that is called for every element of the array.

    • currentValue: The current element being processed.

    • index (optional): The index of the current element being processed.

    • array (optional): The array map was called upon.

  • thisArg (optional): Value to use as this when executing the callback.

const a = [1, 2, 3, 4, 5, 6, 7];

const b = a.map((element) => {
    return element * element;
});
/* Output:
[1,  4,  9, 16, 25, 36, 49]
*/

Let's take a better example, suppose we have an array that has multiple objects:

const students = [
    {
        name: "a",
        marks: 20
    }, {
        name: "b",
        marks: 84
    }, {
        name: "c",
        marks: 39
    }, {
        name: "d",
        marks: 68
    }, {
        name: "e",
        marks: 90
    }, 
];

And we want to add one more property as isPassed and students who has marks less than 50 will have the value of isPassed = false and vise versa, so we can use map function here:

const isStudentPassed = students.map((element) => {
    if(element.marks < 50) {
        element.isPassed = false
    } else {
        element.isPassed = true
    }
    return element
});

console.log(isStudentPassed);
/* Output: 
[
  { name: 'a', marks: 20, isPassed: false },
  { name: 'b', marks: 84, isPassed: true },
  { name: 'c', marks: 39, isPassed: false },
  { name: 'd', marks: 68, isPassed: true },
  { name: 'e', marks: 90, isPassed: true }
] */
  • This is a very basic operation inside the .map method, we are checking for each students if their marks are less than 50 then create a new property inside the object with the value of false, else fill the value with true.

filter

The filter method creates a new array with all elements that pass the test implemented by the provided function.

syntax

array.map(callback(currentValue, index, array), thisArg);
  • callback: Function that is called for every element of the array.

    • element: The current element being processed.

    • index (optional): The index of the current element being processed.

    • array (optional): The array filter was called upon.

  • thisArg (optional): Value to use as this when executing the callback.

const a = [1, 2, 3, 4, 5, 6, 7];

const b = a.filter((element) => {
    return element % 2 == 0;
});

console.log(b);

/* Output:
[ 2, 4, 6 ]
*/

Let's again understand it with a better example, suppose we again have an array that has multiple objects where students name and marks are included:

const students = [
    {
        name: "a",
        marks: 20
    }, {
        name: "b",
        marks: 84
    }, {
        name: "c",
        marks: 39
    }, {
        name: "d",
        marks: 68
    }, {
        name: "e",
        marks: 90
    }, 
];

And we want a list where all the students who have less than 50 marks, we can use filter method to filter out the data who has less than 50 marks:

const e = students.filter((element) => {
    return element.marks < 50;
});

console.log(e);
/* Output:
[ { name: 'a', marks: 20 }, { name: 'c', marks: 39 } ]
*/

If we wanted a list where students have more than 50 marks we can simply do:

const e = students.filter((element) => {
    return element.marks > 50;
});

console.log(e);

/* Output:
[
  { name: 'b', marks: 84 },
  { name: 'd', marks: 68 },
  { name: 'e', marks: 90 }
]
*/

find

The find method returns the value of the first element in the provided array that satisfies the provided testing function. If no values satisfy the testing function, undefined is returned.

const a = [1, 2, 3, 4, 5, 6, 7];

// Finding an element that exists inside the array.
const b = a.find((element) => {
    return element % 2 == 0;
});

// Finding an element inside the array that does not exist.
const c = a.find((element) => {
    return element % 9 == 0;
});

console.log(b); // Output: 2
console.log(c); // Output: undefined

Again let's understand this with a better example, suppose we want to find the first student from inside of our object where marks is less than 50, here's a demonstration:

const students = [
    {
        name: "a",
        marks: 20
    }, {
        name: "b",
        marks: 84
    }, {
        name: "c",
        marks: 39
    }, {
        name: "d",
        marks: 68
    }, {
        name: "e",
        marks: 90
    }, 
];
const a = students.find((element) => {
    return element.marks < 50;
});

console.log(a);
// Output: { name: 'a', marks: 20 }
  • As mentioned above, the .find() method will keep on searching until it has satisfied the provided testing function, once the testing function has been satisfied it will pick it up and stop the loop for searching for next element.

Here's a summary for these three array methods:

  • map: Creates a new array by applying a function to each element of the original array.

  • filter: Creates a new array with elements that pass a test provided by a function.

  • find: Returns the first element in the array that satisfies a provided testing function.

These methods provide a concise and readable way to manipulate and query arrays, making your code more functional and easier to maintain.

ES6 Concepts

Default Parameters

Default parameters allow you to initialize function parameters with default values if no arguments are passed or if undefined is passed.

function greet(name = 'Guest') {
  return `Hello, ${name}!`;
}

console.log(greet());        // Output: Hello, Guest!
console.log(greet("Aquib")); // Output: Hello, Aquib!

Spread Operator

The spread operator (...) allows an iterable such as an array or string to be expanded in places where zero or more arguments (for function calls) or elements (for array literals) are expected. It can also be used to spread objects.

Example with Arrays:

const numbers = [1, 2, 3];
const moreNumbers = [...numbers, 4, 5, 6];
console.log(moreNumbers); // Output: [1, 2, 3, 4, 5, 6]

function sum(x, y, z) {
  return x + y + z;
}
const args = [1, 2, 3];
console.log(sum(...args)); // Output: 6

Example with Objects

const obj1 = { a: 1, b: 2 };
const obj2 = { c: 3, d: 4 };
const combinedObj = { ...obj1, ...obj2 };
console.log(combinedObj); // Output: { a: 1, b: 2, c: 3, d: 4 }

Rest Operator

The rest operator (...) allows you to represent an indefinite number of arguments as an array. It's useful for functions that accept a variable number of arguments.

function sum(...numbers) {
  return numbers.reduce((acc, num) => acc + num, 0);
}

console.log(sum(1, 2, 3));    // Output: 6
console.log(sum(1, 2, 3, 4)); // Output: 10

Practical Example Combining Spread and Rest Operators

//In parameters it will convert it into array
//spread
function greet(...a) {
    a.map((element) => {
        console.log(`Hello ${element}`);
    });
}

const arr = ["Aquib", "Ajay", "Chinu"];

//While passing it will destructure the array
//rest
greet(...arr);

/* Output: 
Hello Aquib
Hello Ajay
Hello Chinu
*/

Here's a weird example using an Object

const animal = {
    name: "Simba",
    age: 16
};

const information = {
    //Destructuring the object.
    //spread
    ...animal,
    address: "Mountan",
    legs: 4
};

function greet2(animal) {
    console.log(`Hello ${animal.name}`);
}

greet2(information); // Output: Hello Simba
  • Spread Operator: Expands iterables (arrays, objects) into individual elements or properties.

  • Rest Operator: Aggregates multiple elements into an array or object, used primarily in function parameters to handle an indefinite number of arguments.