# Section 9: Error Handling with Express The lecture focuses on debugging Node.js applications using a tool called NDB (Node Debugger). It highlights the inevitability of bugs in coding and the significance of having effective debugging tools. Key points from the lecture include: - **Installation of NDB:** The instructor discusses installing NDB via npm, with options for global or local installation based on user permissions. - **Setting Up Debugging:** A debugging script is set up in the package.json file to start NDB with the application's entry point, such as server.js. - **User Interface:** When NDB is running, it opens a new Chrome window that offers a user-friendly interface for debugging, showing the file system, npm scripts, and a console. - **Editing in Debugger:** The ability to edit files directly within the debugger is shown, allowing real-time code updates. - **Using Breakpoints:** The lecture details how to set breakpoints that pause code execution to inspect variables and application state, which is demonstrated with an example. - **Analyzing Code:** The instructor navigates through the debugger to analyze the request and response objects, vital for identifying issues. - **Hands-On Debugging:** A bug is deliberately introduced to illustrate the debugging process, where the instructor steps through the code to find the bug using variable examination and execution flow analysis. - **Summary:** The effectiveness of NDB as a debugging tool for Node.js is emphasized, encouraging users to leverage this tool for better understanding and troubleshooting. ## Installing NDB ```console npm i ndb --save-dev ``` - add the debug script in `package.js`: ```js "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "start": "SET NODE_ENV=development&&nodemon server.js", "start:prod": "SET NODE_ENV=production&&nodemon server.js", "debug": "ndb server.js" }, ``` - run the script ```console npm run debug ``` a new chrome window should open, called a headless chrome ![image](https://hackmd.io/_uploads/SyoD1k3agx.png) - add a break point (the green line) then run the script: ![001](https://hackmd.io/_uploads/B12hhGp6xe.png) - Taking a look at the variables: this is where we have access to the five variables `require`, `__dirname`, `__filename`, `module` and `export` ![001](https://hackmd.io/_uploads/S1D4nJAale.png) `scope` -> `local` -> `app` -> `_router` -> `stack` ![image](https://hackmd.io/_uploads/Sy26mJn6xx.png) This is our middleware stack from `app.js` `scope` -> `global` -> `process` -> `env` This is where our environment variables is stored (not showing for security reasons) - Testing the debugging using Postman: ![002](https://hackmd.io/_uploads/SJ6jnJCTgl.png) - Setting up a breakpoint in the `tourController.js` to see what the variables look like after the query is done. ![003](https://hackmd.io/_uploads/HJbRhk0Tgl.png) Making a get request using Postman gives us the `req` and `res` variables as well as the `tour` object: ![004](https://hackmd.io/_uploads/BkabpkRalg.png) ![image](https://hackmd.io/_uploads/H1luGm6aex.png) - Introducing a bug to learn debugging In the `apiFeatures.js` we remove the space passed into the `.join()` method: ```js limitFields() { // 4 Field Limiting if (this.queryString.fields) { // getting fields string, formatting them separated with space const fields = this.queryString.fields.split(",").join(""); // query = query.select("name duration difficulty price"); this.query = this.query.select(fields); } else { // if no field is specified, remove the __v property (-) this.query = this.query.select("-__v"); } return this; } ``` Sending `get` request using Postman: ![image](https://hackmd.io/_uploads/S1wnXQ6age.png) We are not receiving the proper data. Add a break point here in `tourController.js`; ![image](https://hackmd.io/_uploads/SJ1C4mp6gx.png) **** Click on `step` to execute the next line of code: ![image](https://hackmd.io/_uploads/S184HmTalx.png) We see that we have no space, the correct query is `name duration`: ![image](https://hackmd.io/_uploads/H1v9HXa6ll.png) Fixing the code returns the proper data: ![005](https://hackmd.io/_uploads/BJy8Ty0ael.png) ## Handling Unhandled Routes - **404 Not Found Error:** When accessing undefined routes, users encounter a 404 error, traditionally displayed in HTML. The speaker emphasizes that for APIs, a JSON response is more appropriate. - **Middleware Execution Order:** Understanding the middleware execution order in Express is crucial for implementing a catch-all handler for undefined routes. - **Creating a Catch-All Handler:** The speaker demonstrates how to use app.all to create a handler that responds to all HTTP methods (GET, POST, DELETE, etc.) for any undefined routes by employing a wildcard character (*). - **JSON Response:** When an undefined route is accessed, the handler sends a JSON response with a 404 status, including a message that the requested URL could not be found. It utilizes req.originalUrl to indicate the specific URL the user tried to access. - **Placement of the Handler:** The importance of placing this catch-all handler at the end of the middleware stack is emphasized, ensuring that it only activates when no other routes have been matched. Requesting for URLs that you dont have defined returns this html including a `404` error: ![image](https://hackmd.io/_uploads/SyhaIPATeg.png) When requesting for a random string like the one below, it returns a `CastError`, because we actually have a route that accepts `id` parameter ![image](https://hackmd.io/_uploads/r1y8DvRage.png) Since we are building an API, it does not make sense to send back an HTML, fixing that: `app.js`: ```js app.use("/api/v1/tours", tourRouter); app.use("/api/v1/users", userRouter); // Placing the code below the other route definition because that means the request did not hit the above routes // .all() means all the http method (get, post, put...) app.all("*", (req, res, next) => { res.status(404).json({ status: "fail", message: `Can't find ${req.originalUrl} on this server.`, }); }); module.exports = app; ``` ![image](https://hackmd.io/_uploads/BJKCFPC6xl.png) ## An Overview of Error Handling - **Error Handling Approaches:** The speaker notes that earlier methods involved sending error messages directly from route handlers as JSON, which is not the best practice. - **Types of Errors:** Two main types of errors are highlighted: - **Operational Errors:** These are predictable issues resulting from user actions, system failures, or network problems (e.g., accessing invalid routes). They are not due to code bugs and should be anticipated and proactively managed. - **Programming Errors:** These are bugs introduced by developers, such as referencing undefined variables or using asynchronous functions incorrectly. - **Focus on Operational Errors:** In the context of Express, handling operational errors is emphasized, as they are easier to catch and manage. - **Global Error Handling Middleware:** Express provides built-in support for centralizing error management through global error handling middleware, which catches errors from various parts of the application, including route handlers and model validators. This ensures all errors are handled in one place. - **Informative Responses:** The speaker explains that effective error handling means sending informative responses to clients, which may involve retrying operations, crashing the server, or sometimes ignoring the error. - **Clean Separation of Concerns:** The global error handling middleware helps keep business logic clear, allowing developers to focus on core functionality without intertwining it with error handling code. ![image](https://hackmd.io/_uploads/HJqJiP06xl.png) ## Implementing a Global Error Handling Middleware - **Importance of Error Handling:** The lecture underscores that a robust error-handling strategy is essential for any backend application and aims to eliminate decentralized error handling within individual route handlers. - **Types of Errors:** The speaker distinguishes between two types of errors in Express applications: - **Operational Errors:** Predictable issues that can occur due to user actions, system failures, etc. Examples include accessing invalid routes or submitting incorrect data. - **Programming Errors:** Bugs introduced by developers in the code. - **Centralizing Error Handling:** The goal is to write a global error handling middleware that can manage operational errors centrally rather than sending response messages directly from each route handler. This reduces redundancy and keeps the code clean. - **Building the Middleware:** The speaker demonstrates how to create the global error handling middleware in Express: - Using `app.use` to define a middleware function. - Ensuring the middleware function accepts four arguments, which is the conventional way Express identifies it as an error handler. - **Functionality of the Middleware:** Once implemented, this middleware will catch errors that occur throughout the application, allowing for a single point where errors can be processed and responded to, improving efficiency and organization. `app.js`: ```js app.all("*", (req, res, next) => { //Creating an error const err = new Error(`Cannot find ${req.originalUrl} on this server.`); err.status = "fail"; err.statusCode = 404; // if the next() receives an argument, it automatically knows that an error occured next(err); }); // by specifying four arguments, express automatically recognizes it as an error handling middleware app.use((err, req, res, next) => { err.statusCode = err.statusCode || 500; err.status = err.status || "error"; res.status(err.statusCode).json({ status: err.status, message: err.message, }); }); ``` ![image](https://hackmd.io/_uploads/HymLldCpxl.png) ## Better Errors and Refactoring - **Improved Error Messaging:** The speaker discusses the importance of providing clear and informative error messages to enhance the developer's understanding during debugging. This can involve improving the formatting and structure of error messages to make them more user-friendly. - **Refactoring Code:** The video encourages the practice of refactoring, where existing code is improved without changing its external behavior. This includes organizing code for readability, reducing duplication, and making it easier to manage long-term. - **Best Practices:** The speaker shares best practices for writing cleaner code and handling errors, which may include: - Centralizing error handling. - Utilizing middleware effectively. - Ensuring that functions and blocks of code are not overly complex. - **Consistency:** Maintaining consistent coding styles and patterns helps both in the readability and maintainability of the code base, making it easier for others (or the same developer later) to understand and work with the code. - **Real-World Applications:** The video provides practical examples to illustrate how better error handling and code refactoring can lead to improved application performance and user experience. Making an error class: `appError.js`: ```js class AppError extends Error { constructor(message, statusCode) { super(message); this.statusCode = statusCode; this.status = `${statusCode}`.startsWith("4") ? "fail" : "error"; this.isOperational = true; Error.captureStackTrace(this, this.constructor); } } module.exports = AppError; ``` `app.js`: ```js const AppError = require("./utils/appError"); app.all("*", (req, res, next) => { next(new AppError(`Cannot find ${req.originalUrl} on this server.`, 404)); }); ``` ![image](https://hackmd.io/_uploads/B10ASuA6lg.png) Making an error controller file in the `controllers` folder: `errorContoller.js`: ```js (err, req, res, next) => { err.statusCode = err.statusCode || 500; err.status = err.status || "error"; res.status(err.statusCode).json({ status: err.status, message: err.message, }); } ``` `app.js`: ```js const globalErrorHandler = require("./controllers/errorController"); app.use(globalErrorHandler); ``` ![image](https://hackmd.io/_uploads/H1tpUOR6ee.png) ## Catching Errors in Async Functions - **Issue with Try-Catch Blocks:** While try-catch blocks are effective for catching errors, they can clutter code and lead to redundancy. - **Introduction of catchAsync Function:** The instructor presents the catchAsync function, which wraps around async functions to centralize error handling. This function simplifies code by removing the need for try-catch in each async function. - **How catchAsync Works:** It takes an async function as an argument and returns a new function that Express can use. This newly created function executes the original async function and handles any errors using the promise's catch method. - **Proper Error Propagation:** It’s crucial to pass the request, response, and next parameters with catchAsync to ensure errors are correctly propagated to the global error handling middleware. - **Practical Implementation:** The instructor demonstrates how to implement catchAsync in a tour controller, allowing users to see the error handling mechanism in action. - **Future Considerations:** While errors are now managed well, the lecture notes that the default 500 status code response for errors will be addressed in upcoming lectures. `tourController.js`: ```js //wraping the async function for error handling const catchAsync = (fn) => (req, res, next) => { fn(req, res, next).catch((err) => next(err)); }; exports.createTour = catchAsync(async (req, res, next) => { const newTour = await Tour.create(req.body); res.status(201).json({ status: "success", data: { tour: newTour, }, }); }); ``` Testing an error in Postman: ![image](https://hackmd.io/_uploads/rJaIZsPCxx.png) Placing the function in its own file in `utils`: `catchAsync.js`: ```js module.exports = (fn) => (req, res, next) => { fn(req, res, next).catch((err) => next(err)); }; ``` Then importing the function: `tourController.js` ```js const catchAsync = require("../utils/catchAsync"); ``` The final file: `tourController.js`: ```js const Tour = require("../models/tourModel"); const APIFeatures = require("../utils/apiFeatures"); const catchAsync = require("../utils/catchAsync"); exports.aliasTopTours = (req, res, next) => { //localhost:3000/api/v1/tours?limit=5&sort=-ratingsAverage,price req.query.limit = "5"; req.query.sort = "-ratingsAverage,price"; req.query.fields = "name,price,ratingsAverage,summary,difficulty"; next(); }; exports.getAllTours = catchAsync(async (req, res, next) => { //Executing the query const features = new APIFeatures(Tour.find(), req.query) .filter() .sort() .limitFields() .paginate(); const tours = await features.query; //Sending the response res.status(200).json({ status: "success", results: tours.length, data: { tours, }, }); }); exports.getTour = catchAsync(async (req, res, next) => { //Tour.findOne({_id: req.params.id}) const tour = await Tour.findById(req.params.id); res.status(200).json({ status: "success", data: { tour, }, }); }); exports.createTour = catchAsync(async (req, res, next) => { const newTour = await Tour.create(req.body); res.status(201).json({ status: "success", data: { tour: newTour, }, }); }); exports.updateTour = catchAsync(async (req, res, next) => { const tour = await Tour.findByIdAndUpdate(req.params.id, req.body, { new: true, runValidators: true, }); res.status(200).json({ status: "success", data: { tour, }, }); }); exports.deleteTour = catchAsync(async (req, res, next) => { await Tour.findOneAndDelete(req.params.id); res.status(204).json({ status: "success", data: null, }); }); exports.getTourStats = catchAsync(async (req, res, next) => { const stats = await Tour.aggregate([ { $match: { ratingsAverage: { $gte: 4.5 } } }, { $group: { _id: "$difficulty", numTours: { $sum: 1 }, numRatings: { $sum: "$ratingsQuantity" }, avgRating: { $avg: "$ratingsAverage" }, avgPrice: { $avg: "$price" }, minPrice: { $min: "$price" }, maxPrice: { $max: "$price" }, }, }, { $sort: { avgPrice: 1, }, }, // { $match: { _id: { $ne: "easy" } } }, ]); res.status(200).json({ status: "success", data: { stats, }, }); }); exports.getMonthlyPlan = catchAsync(async (req, res, next) => { const year = req.params.year * 1; // 2021 const plan = await Tour.aggregate([ { $unwind: "$startDates" }, { $match: { // matching the dates within the given year startDates: { $gte: new Date(`${year}-01-01`), $lte: new Date(`${year}-12-31`), }, }, }, { $group: { _id: { $month: "$startDates" }, numTourStarts: { $sum: 1 }, tours: { $push: "$name" }, }, }, { $addFields: { month: "$_id" }, }, { $project: { _id: 0 }, }, { $sort: { numTourStarts: -1 }, }, { $limit: 12, }, ]); res.status(200).json({ status: "success", data: { plan, }, }); }); ``` ## Adding 404 Not Found Errors - **Use of catchAsync:** The instructor reiterates the importance of wrapping asynchronous functions using the catchAsync function to avoid bugs, sharing a personal experience regarding issues caused by not properly handling a non-async function. - **Handling Non-Existent Tours:** The main focus is on managing cases where a requested tour does not exist in the database. The instructor demonstrates checking for null values when querying for a tour by its ID. - **Creating AppError Instances:** If a tour is not found, the instructor shows how to create an AppError instance, specifying a message indicating that the tour was not found and assigning a 404 status code. This error is passed to the next middleware to activate the global error handling process. - **Implementing in Other Functions:** The approach to 404 error handling is also applied in functions for updating and deleting tours, emphasizing the significance of defining the tour variable to prevent ESLint errors. - **No 404 Error for getAllTour:** The instructor discusses why a 404 error isn't needed in the getAllTour handler when no tours are found, as returning a 200 status with an empty result is appropriate since the request itself is valid. - **Conclusion:** The lecture wraps up by summarizing the error handling process and the role of global error handling middleware in effectively managing errors within the application. Implmenting 404 error on the `getTour()`: `tourContoller.js`: ```js exports.getTour = catchAsync(async (req, res, next) => { //Tour.findOne({_id: req.params.id}) const tour = await Tour.findById(req.params.id); if (!tour) { return next(new AppError("No tour found with that ID", 404)); } res.status(200).json({ status: "success", data: { tour, }, }); }); ``` Inputting an ID that doesnt exist in Postman: ![image](https://hackmd.io/_uploads/SkYI_sP0gl.png) ## Errors During Development vs Production - **Importance of Error Handling:** The instructor emphasizes that having a robust error-handling strategy is crucial for any backend application, highlighting that error handling has its own dedicated section in the course. - **Differentiating Environments:** When developing an error-handling strategy, it's important to distinguish between development and production environments. Each has specific requirements for handling and displaying errors. - **Operational vs. Unknown Errors:** The approach categorizes errors into operational (trusted errors regarding the application) and unknown errors (that cannot be trusted). This helps in deciding how to respond to different types of errors. - **Rendering Error Messages:** For production, if an operational error occurs, a trusted error message is sent. If it’s an unknown error, a generic message is provided, thus protecting sensitive information while still communicating that an error occurred. - **Making Error Messages Useful:** The instructor discusses the need to properly copy error messages, ensuring that relevant information makes it through to the client, which is important for debugging and user feedback. - **Practical Application:** The lecture concludes with adjustments made during the coding process to ensure that error messages work correctly across both environments, demonstrating the necessity of thorough testing and validation of error handling. `errorController.js`: ```js // more detailed error during development and fewer in production if (process.env.NODE_ENV === "development") { res.status(err.statusCode).json({ status: err.status, err: err, message: err.message, stack: err.stack, }); } else if (process.env.NODE_ENV === "production") { res.status(err.statusCode).json({ status: err.status, message: err.message, }); } ``` Dividing the feature into different functions: ```js const sendErrorDev = (err, res) => { res.status(err.statusCode).json({ status: err.status, err: err, message: err.message, stack: err.stack, }); }; const sendErrorProd = (err, res) => { res.status(err.statusCode).json({ status: err.status, message: err.message, }); }; module.exports = (err, req, res, next) => { err.statusCode = err.statusCode || 500; err.status = err.status || "error"; // more detailed error during development and fewer in production if (process.env.NODE_ENV === "development") { sendErrorDev(err, res); } else if (process.env.NODE_ENV === "production") { sendErrorProd(err, res); } }; ``` The final file: `errorController.js`: ```js const sendErrorDev = (err, res) => { res.status(err.statusCode).json({ status: err.status, err: err, message: err.message, stack: err.stack, }); }; const sendErrorProd = (err, res) => { // Operational error, trusted error: send message to client if (err.isOperational) { res.status(err.statusCode).json({ status: err.status, message: err.message, }); // Programming or other unknown error: dont send the details to the client } else { // Log error console.log("ERROR!", err); // Send a generic message res.status(500).json({ status: "error", message: "Somethin went very wrong!", }); } }; module.exports = (err, req, res, next) => { err.statusCode = err.statusCode || 500; err.status = err.status || "error"; // more detailed error during development and fewer in production if (process.env.NODE_ENV === "development") { sendErrorDev(err, res); } else if (process.env.NODE_ENV === "production") { sendErrorProd(err, res); } }; ``` ## Handling Invalid Database IDs The video on "Handling Invalid Database IDs" focuses on managing three types of operational errors that can arise when using Mongoose with MongoDB. - **CastError:** This error occurs when an invalid ID is provided that Mongoose cannot convert into a valid MongoDB ID. The lecture stresses the importance of sending meaningful error messages to clients to enhance user experience. - **Duplicate Key Error:** This happens when trying to create a document with a name that already exists in the database. Again, the need for clear and informative error messages is emphasized. - **ValidationError:** This occurs when input values do not meet specified validation criteria, such as exceeding allowed limits. The lecture demonstrates how to transform these errors into user-friendly messages. To effectively handle these errors, the lecture suggests creating a function to check the error type and generate an appropriate AppError with a clear message and a 400 Bad Request status code. This ensures clients receive understandable feedback during errors. It concludes by demonstrating how to test the error handling in a production environment, paving the way for the next topic on handling duplicate field names. Simulating the three errors: - Inputting an invalid ID: ![image](https://hackmd.io/_uploads/rJ51djKRee.png) - Inputting a duplicate name for a tour: ![image](https://hackmd.io/_uploads/BJSKojYRgx.png) - Inputting a `ratingsAverage` of `6`, max is `5` making the input invalid and `difficulty` to somethings else other than `easy`, `medium` and `difficult`: ![image](https://hackmd.io/_uploads/rJZXasKCxx.png) ```json { "status": "error", "err": { "errors": { "difficulty": { "name": "ValidatorError", "message": "Difficult must be easy, medium, difficult", "properties": { "message": "Difficult must be easy, medium, difficult", "type": "enum", "enumValues": [ "easy", "medium", "difficult" ], "path": "difficulty", "value": "maybe" }, "kind": "enum", "path": "difficulty", "value": "maybe" }, "ratingsAverage": { "name": "ValidatorError", "message": "Rating must be below 5.0", "properties": { "message": "Rating must be below 5.0", "type": "max", "max": 5, "path": "ratingsAverage", "value": 6 }, "kind": "max", "path": "ratingsAverage", "value": 6 } }, "_message": "Validation failed", "statusCode": 500, "status": "error", "name": "ValidationError", "message": "Validation failed: difficulty: Difficult must be easy, medium, difficult, ratingsAverage: Rating must be below 5.0" }, "message": "Validation failed: difficulty: Difficult must be easy, medium, difficult, ratingsAverage: Rating must be below 5.0", "stack": "ValidationError: Validation failed: difficulty: Difficult must be easy, medium, difficult, ratingsAverage: Rating must be below 5.0\n at _done (C:\\Users\\theol\\OneDrive\\Documents\\WebDev\\Nodejs\\Natours\\node_modules\\mongoose\\lib\\helpers\\updateValidators.js:236:19)\n at C:\\Users\\theol\\OneDrive\\Documents\\WebDev\\Nodejs\\Natours\\node_modules\\mongoose\\lib\\helpers\\updateValidators.js:212:11\n at schemaPath.doValidate.updateValidator (C:\\Users\\theol\\OneDrive\\Documents\\WebDev\\Nodejs\\Natours\\node_modules\\mongoose\\lib\\helpers\\updateValidators.js:170:13)\n at C:\\Users\\theol\\OneDrive\\Documents\\WebDev\\Nodejs\\Natours\\node_modules\\mongoose\\lib\\schematype.js:1273:9\n at process.processTicksAndRejections (node:internal/process/task_queues:85:11)" } ``` ## Cast-error: ![image](https://hackmd.io/_uploads/B1zT6itRxg.png) During production, we want to send a specific message when we hit a `CastError`: ```js // more detailed error during development and fewer in production if (process.env.NODE_ENV === "development") { sendErrorDev(err, res); } else if (process.env.NODE_ENV === "production") { let error = err; if (error.name === "CastError") error = handleCastErrorDB(error); sendErrorProd(error, res); } ``` ```js const handleCastErrorDB = (err) => { const message = `Invalid ${err.path}: ${err.value}`; return new AppError(message, 400); }; ``` ![image](https://hackmd.io/_uploads/HyG-EnK0ll.png) ## Handling Duplicate Database Fields The video on "Handling Duplicate Database Fields" focuses on error management for creating database entries that require unique values. When a new entry, such as a tour with a duplicate name, is attempted, an error occurs due to the uniqueness constraint enforced by MongoDB. This error is identified by a specific error code (11,000) generated by MongoDB's underlying driver. To handle this issue, the lecture outlines a function that generates a user-friendly error message when a duplicate value is detected, encouraging users to choose a different name. The speaker highlights the use of regular expressions to extract the duplicate value from the error message. Regular expressions can be complex, so the lecture includes tips on finding suitable regex patterns online to match text within quotes. After finding a working regex pattern, the speaker demonstrates how to extract the duplicate value and logs it to verify functionality. The lecture wraps up with the implementation of the error handling function that provides a structured error message along with the duplicate field value and a 400 status code, indicating a bad request. Additionally, the speaker hints at discussing validation errors in the next video, continuing the topic. ![image](https://hackmd.io/_uploads/SJn_NnKAll.png) We will be using the `code: 1100` to identify the error `errorController.js`: ```js // more detailed error during development and fewer in production if (process.env.NODE_ENV === "development") { sendErrorDev(err, res); } else if (process.env.NODE_ENV === "production") { let error = err; if (error.name === "CastError") error = handleCastErrorDB(error); if (error.code === 11000) error = handleDuplicateFieldsDB(error); sendErrorProd(error, res); } }; ``` ```js const handleDuplicateFieldsDB = (err) => { const message = `Duplicate field value: ${err.keyValue.name}. Please use another value`; return new AppError(message, 400); }; ``` ![image](https://hackmd.io/_uploads/Hypyu2tClg.png) ::: info Did not use `regex` as err object structure was slightly different ::: ## Handling Mongoose Validation Errors The video on "Handling Mongoose Validation Errors" delves into how to manage errors that arise when invalid data is provided in a Mongoose-based application. Here's a breakdown of the key points covered: - **Understanding Validation Errors:** These errors are generated when the data provided to a Mongoose model does not adhere to the defined schema, such as a string being too short. The video demonstrates scenarios where invalid data inputs are rejected, triggering validation errors. - **Operational Errors:** The lecture emphasizes the importance of marking these validation errors as operational. This ensures that the application can provide meaningful feedback to clients instead of generic error messages. It highlights the need for structured handling of these errors to improve user experience. - **Extracting Error Messages:** The instructor shows how to access the error properties returned by Mongoose. Each error corresponds to a specific field in the document being validated. This allows developers to retrieve detailed messages that explain what went wrong, which can then be sent back to the client in a user-friendly format. - **Implementation:** The video includes practical demonstrations of how to implement error handling for various validation scenarios, illustrating how to transform the raw data received from Mongoose into a clear and informative message for the user. - **Production vs. Development:** The importance of distinguishing between development and production error handling is highlighted, stressing that clients should receive tailored messages in a production environment that do not expose unnecessary technical details. Error generated when inputted invalid data our validator does not allow: ![image](https://hackmd.io/_uploads/rJaSKhFRlg.png) JSON Response: ```json { "status": "error", "err": { "errors": { "name": { "name": "ValidatorError", "message": "A tour name must be longer than 10 characters", "properties": { "message": "A tour name must be longer than 10 characters", "type": "minlength", "minlength": 10, "path": "name", "value": "Short" }, "kind": "minlength", "path": "name", "value": "Short" }, "difficulty": { "name": "ValidatorError", "message": "Difficult must be easy, medium, difficult", "properties": { "message": "Difficult must be easy, medium, difficult", "type": "enum", "enumValues": [ "easy", "medium", "difficult" ], "path": "difficulty", "value": "maybe" }, "kind": "enum", "path": "difficulty", "value": "maybe" }, "ratingsAverage": { "name": "ValidatorError", "message": "Rating must be below 5.0", "properties": { "message": "Rating must be below 5.0", "type": "max", "max": 5, "path": "ratingsAverage", "value": 6 }, "kind": "max", "path": "ratingsAverage", "value": 6 } }, "_message": "Validation failed", "statusCode": 500, "status": "error", "name": "ValidationError", "message": "Validation failed: name: A tour name must be longer than 10 characters, difficulty: Difficult must be easy, medium, difficult, ratingsAverage: Rating must be below 5.0" }, "message": "Validation failed: name: A tour name must be longer than 10 characters, difficulty: Difficult must be easy, medium, difficult, ratingsAverage: Rating must be below 5.0", "stack": "ValidationError: Validation failed: name: A tour name must be longer than 10 characters, difficulty: Difficult must be easy, medium, difficult, ratingsAverage: Rating must be below 5.0\n at _done (C:\\Users\\theol\\OneDrive\\Documents\\WebDev\\Nodejs\\Natours\\node_modules\\mongoose\\lib\\helpers\\updateValidators.js:236:19)\n at C:\\Users\\theol\\OneDrive\\Documents\\WebDev\\Nodejs\\Natours\\node_modules\\mongoose\\lib\\helpers\\updateValidators.js:212:11\n at schemaPath.doValidate.updateValidator (C:\\Users\\theol\\OneDrive\\Documents\\WebDev\\Nodejs\\Natours\\node_modules\\mongoose\\lib\\helpers\\updateValidators.js:170:13)\n at C:\\Users\\theol\\OneDrive\\Documents\\WebDev\\Nodejs\\Natours\\node_modules\\mongoose\\lib\\schematype.js:1273:9\n at process.processTicksAndRejections (node:internal/process/task_queues:85:11)" } ``` `errorController.js`: ```js // more detailed error during development and fewer in production if (process.env.NODE_ENV === "development") { sendErrorDev(err, res); } else if (process.env.NODE_ENV === "production") { let error = err; if (error.name === "CastError") error = handleCastErrorDB(error); if (error.code === 11000) error = handleDuplicateFieldsDB(error); if (error.name === "ValidationError") error = handleValidationErrorDB(error); sendErrorProd(error, res); } }; ``` ```js const handleValidationErrorDB = (err) => { const errorFields = Object.values(err.errors).map((el) => el.message); const message = `Invalid input data. ${errorFields.join(". ")}`; return new AppError(message, 400); }; ``` ![image](https://hackmd.io/_uploads/SkKZ0nt0ex.png) ## Errors Outside Express: Unhandled Rejections The video on "Errors Outside Express: Unhandled Rejections" discusses the concept of unhandled promise rejections in Node.js, particularly in the context of error handling outside of Express applications. - **Introduction to Unhandled Rejections:** The video begins by explaining what unhandled promise rejections are. In Node.js, these occur when a promise is rejected, and there’s no .catch() method or error handler to handle that rejection. This is important because it can lead to silent failures in application functionality. - **Errors Beyond Express:** The instructor illustrates that, while Express has mechanisms for handling operational errors within the application, errors can also arise from external sources like MongoDB connection issues. For example, if the application tries to connect to a database with an incorrect password, it results in an unhandled promise rejection.\ - **Testing Unhandled Rejections:** To demonstrate this concept, the instructor proposes changing the MongoDB password to simulate a connection failure, which results in an unhandled promise rejection. The outcome indicates that without proper handling, the error will not be caught by the Express middleware. - **Handling Strategy:** The video mentions that to properly manage these errors, developers should add an event listener in their Node.js application to capture unhandled rejections. This allows them to log these errors and optionally terminate the process to prevent the application from continuing in an uncertain state. - **Conclusion:** The instructor wraps up the lecture by emphasizing that while basic error handling is addressed in earlier sections, unhandled rejections pose a unique challenge that should also be well-managed to ensure a robust application. Changing the password in the `config.env` file to introduce an error: ```console C:\Users\theol\OneDrive\Documents\WebDev\Nodejs\Natours\node_modules\mongodb\lib\core\topologies\server.js:441 new MongoNetworkError( ^ MongoNetworkError: failed to connect to server [ac-kogrkbb-shard-00-02.g71alt2.mongodb.net:27017] on first connect [MongoError: bad auth : authentication failed at Connection.messageHandler (C:\Users\theol\OneDrive\Documents\WebDev\Nodejs\Natours\node_modules\mongodb\lib\core\connection\connection.js:364:19) at Connection.emit (node:events:518:28) at processMessage (C:\Users\theol\OneDrive\Documents\WebDev\Nodejs\Natours\node_modules\mongodb\lib\core\connection\connection.js:456:10) at TLSSocket.<anonymous> (C:\Users\theol\OneDrive\Documents\WebDev\Nodejs\Natours\node_modules\mongodb\lib\core\connection\connection.js:625:15) at TLSSocket.emit (node:events:518:28) at addChunk (node:internal/streams/readable:561:12) at readableAddChunkPushByteMode (node:internal/streams/readable:512:3) at Readable.push (node:internal/streams/readable:392:5) at TLSWrap.onStreamRead (node:internal/stream_base_commons:189:23) { ok: 0, code: 8000, codeName: 'AtlasError' }] ``` Handling unhandled rejections: `server.js`: ```js const port = process.env.PORT || 3000; const server = app.listen(port, () => { console.log(`App is running on port ${port}...`); }); process.on("unhandledRejection", (err) => { console.log(err.name); console.log(err.message); console.log("UNHANDLED REJECTION, Shutting down..."); //shutting down the server first, finishing all pending requests, then server is killed server.close(() => { process.exit(1); }); }); ``` ```console MongoNetworkError failed to connect to server [ac-kogrkbb-shard-00-02.g71alt2.mongodb.net:27017] on first connect [MongoError: bad auth : authentication failed at Connection.messageHandler (C:\Users\theol\OneDrive\Documents\WebDev\Nodejs\Natours\node_modules\mongodb\lib\core\connection\connection.js:364:19) at Connection.emit (node:events:518:28) at processMessage (C:\Users\theol\OneDrive\Documents\WebDev\Nodejs\Natours\node_modules\mongodb\lib\core\connection\connection.js:456:10) at TLSSocket.<anonymous> (C:\Users\theol\OneDrive\Documents\WebDev\Nodejs\Natours\node_modules\mongodb\lib\core\connection\connection.js:625:15) at TLSSocket.emit (node:events:518:28) at addChunk (node:internal/streams/readable:561:12) at readableAddChunkPushByteMode (node:internal/streams/readable:512:3) at Readable.push (node:internal/streams/readable:392:5) at TLSWrap.onStreamRead (node:internal/stream_base_commons:189:23) { ok: 0, code: 8000, codeName: 'AtlasError' }] ``` ## Catching Uncaught Exceptions The video on "Catching Uncaught Exceptions" covers strategies for handling exceptions that occur in Node.js applications outside of the typical error handling mechanisms provided by Express. Here's a summary of the key points discussed: - **Understanding Uncaught Exceptions:** The instructor explains that uncaught exceptions occur when there is an error in the asynchronous code, and no error handling is implemented. This can cause the application to crash, leading to poor user experience. - **Using Try-Catch Blocks:** The video emphasizes using try-catch blocks around asynchronous code, particularly when using async/await. The structure is similar to synchronous code error handling, allowing developers to catch and manage errors gracefully. - **Immediate Invocation:** The instructor demonstrates a method where a function is created and immediately invoked. This pattern allows for a cleaner way to handle errors, ensuring that errors can be caught within the invoked function's scope. - **Logging Errors:** It is essential to log the errors within the catch block to understand what went wrong. Developers are encouraged to console log the error details, which can assist in debugging the issue. - **Preparing for Future JavaScript Changes:** The instructor mentions that future JavaScript versions may change how error handling in asynchronous functions is handled, but the current method requires the error to be explicitly included in the catch. - **Final Thoughts:** The video concludes by reiterating that handling uncaught exceptions is vital to building robust and resilient applications, and it highlights the importance of understanding error handling principles for efficient application development. Simulating an `uncaughtException`: ```js process.on("uncaughtException", (err) => { console.log(" UNCAUGHT EXCEPTION, Shutting down..."); console.log(err.name); console.log(err.message); //shutting down the server first, finishing all pending requests, then server is killed server.close(() => { process.exit(1); }); }); console.log(x); ``` ```console [nodemon] starting `node server.js` development [dotenv@17.2.3] injecting env (4) from config.env -- tip: 👥 sync secrets across teammates & machines: https://dotenvx.com/ops UNCAUGHT EXCEPTION, Shutting down... ReferenceError x is not defined [nodemon] app crashed - waiting for file changes before starting... ``` Entire `server.js`: ```js const mongoose = require("mongoose"); const dotenv = require("dotenv"); const app = require("./app"); process.on("uncaughtException", (err) => { console.log(" UNCAUGHT EXCEPTION, Shutting down..."); console.log(err.name); console.log(err.message); process.exit(1); }); dotenv.config({ path: "./config.env", }); //getting the connection string and replacing the password in it const DB = process.env.DATABASE.replace( "<PASSWORD>", process.env.DATABASE_PASSWORD, ); //pass in the database connection string mongoose .connect(DB, { useNewUrlParser: true, useCreateIndex: true, useFindAndModify: false, }) .then(() => { console.log("DB connection successful!"); }); const port = process.env.PORT || 3000; const server = app.listen(port, () => { console.log(`App is running on port ${port}...`); }); process.on("unhandledRejection", (err) => { console.log("UNHANDLED REJECTION, Shutting down..."); console.log(err.name); console.log(err.message); //shutting down the server first, finishing all pending requests, then server is killed server.close(() => { process.exit(1); }); }); ```