# 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

```javascript
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.
    

```javascript
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.

```javascript
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`.

```javascript
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.

```javascript
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.

```javascript
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.
    

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

## 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.

```javascript
// math.js
export const PI = 3.14;

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

**Default Export**

```javascript
// 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.

```javascript
// 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

```javascript
// 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.

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

export default function multiply(a, b) {
  return a * b;
}
```

```javascript
// 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

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

export { PI as PI_VALUE, E as EULER_NUMBER };
```

```javascript
// 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**

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

export { PI, E };
```

```javascript
// 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.**

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

## `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

```javascript
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.
    

```javascript
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:**

```javascript
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:

```javascript
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**

```javascript
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.
    

```javascript
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:**

```javascript
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:**

```javascript
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:**

```javascript
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.

```javascript
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:

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

```javascript
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.
    

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

### 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.

```javascript
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:**

```javascript
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**

```javascript
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.

```javascript
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

```javascript
//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**

```javascript
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.
