22
loading...
This website collects cookies to deliver better user experience
Error
object. Common examples include built-in error classes, such as ReferenceError
, RangeError
, TypeError
, URIError
, EvalError
, and SyntaxError
. User-defined errors can also be created by extending the base Error
object, a built-in error class, or another custom error. When creating errors in this manner, you should pass a message string that describes the error. This message can be accessed through the message
property on the object. The Error
object also contains a name
and a stack
property that indicate the name of the error and the point in the code at which it is created, respectively.const userError = new TypeError("Something happened!");
console.log(userError.name); // TypeError
console.log(userError.message); // Something happened!
console.log(userError.stack);
/*TypeError: Something happened!
at Object.<anonymous> (/home/ayo/dev/demo/main.js:2:19)
<truncated for brevity>
at node:internal/main/run_main_module:17:47 */
Error
object, you can pass it to a function or return it from a function. You can also throw
it, which causes the Error
object to become an exception. Once you throw an error, it bubbles up the stack until it is caught somewhere. If you fail to catch it, it becomes an uncaught exception, which may cause your application to crash!try/catch
block. If the error is allowed to bubble up the stack without being caught, it becomes an uncaughtException
, which causes the application to exit prematurely. For example, the built-in JSON.parse()
method throws an error if its string argument is not a valid JSON object.function parseJSON(data) {
return JSON.parse(data);
}
try {
const result = parseJSON('A string');
} catch (err) {
console.log(err.message); // Unexpected token A in JSON at position 0
}
throw
keyword before an instance of an error. This pattern of error reporting and handling is idiomatic for functions that perform synchronous operations.function square(num) {
if (typeof num !== 'number') {
throw new TypeError(`Expected number but got: ${typeof num}`);
}
return num * num;
}
try {
square('8');
} catch (err) {
console.log(err.message); // Expected number but got: string
}
function (err, result) {}
err
argument and result
will be undefined.
However, if no error occurs, err
will be null
or undefined
, and result
will contain the expected result of the operation. This pattern can be demonstrated by reading the contents of a file using the built-in fs.readFile()
method:const fs = require('fs');
fs.readFile('/path/to/file.txt', (err, result) => {
if (err) {
console.error(err);
return;
}
// Log the file contents if no error
console.log(result);
});
readFile()
method expects a callback function as its last argument, which adheres to the error-first function signature discussed earlier. In this scenario, the result
argument contains the contents of the file read if no error occurs. Otherwise, it is undefined
, and the err
argument is populated with an error object containing information about the problem (e.g., file not found or insufficient permissions).result
before checking for errors.function square(num, callback) {
if (typeof callback !== 'function') {
throw new TypeError(`Callback must be a function. Got: ${typeof callback}`);
}
// simulate async operation
setTimeout(() => {
if (typeof num !== 'number') {
// if an error occurs, it is passed as the first argument to the callback
callback(new TypeError(`Expected number but got: ${typeof num}`));
return;
}
const result = num * num;
// callback is invoked after the operation completes with the result
callback(null, result);
}, 100);
}
square
function would need to pass a callback function to access its result or error. Note that a runtime exception will occur if the callback argument is not a function.square('8', (err, result) => {
if (err) {
console.error(err)
return
}
console.log(result);
});
try/catch
block. An asynchronous exception is not catchable because the surrounding try/catch
block exits before the callback is executed. Therefore, the exception will propagate to the top of the stack, causing your application to crash unless a handler has been registered for process.on('uncaughtException')
, which will be discussed later.try {
square('8', (err, result) => {
if (err) {
throw err; // not recommended
}
console.log(result);
});
} catch (err) {
// This won't work
console.error("Caught error: ", err);
}
async/await
pattern. Any Node.js API that utilizes error-first callbacks for asynchronous error handling can be converted to promises using the built-in util.promisify()
method. For example, here's how the fs.readFile()
method can be made to utilize promises:const fs = require('fs');
const util = require('util');
const readFile = util.promisify(fs.readFile);
readFile
variable is a promisified version of fs.readFile()
in which promise rejections are used to report errors. These errors can be caught by chaining a catch
method, as shown below:readFile('/path/to/file.txt')
.then((result) => console.log(result))
.catch((err) => console.error(err));
async
function, such as the one shown below. This is the predominant way to use promises in modern JavaScript because the code reads like synchronous code, and the familiar try/catch
mechanism can be used to handle errors. It is important to use await
before the asynchronous method so that the promise is settled (fulfilled or rejected) before the function resumes its execution. If the promise rejects, the await
expression throws the rejected value, which is subsequently caught in a surrounding catch
block.(async function callReadFile() {
try {
const result = await readFile('/path/to/file.txt');
console.log(result);
} catch (err) {
console.error(err);
}
})();
reject
with an Error
object. Otherwise, resolve
the promise with the result so that it's accessible in the chained .then
method or directly as the value of the async function when using async/await
.function square(num) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (typeof num !== 'number') {
reject(new TypeError(`Expected number but got: ${typeof num}`));
}
const result = num * num;
resolve(result);
}, 100);
});
}
square('8')
.then((result) => console.log(result))
.catch((err) => console.error(err));
const { EventEmitter } = require('events');
function emitCount() {
const emitter = new EventEmitter();
let count = 0;
// Async operation
const interval = setInterval(() => {
count++;
if (count % 4 == 0) {
emitter.emit(
'error',
new Error(`Something went wrong on count: ${count}`)
);
return;
}
emitter.emit('success', count);
if (count === 10) {
clearInterval(interval);
emitter.emit('end');
}
}, 1000);
return emitter;
}
emitCount()
function returns a new event emitter that reports both success and failure events in the asynchronous operation. The function increments the count
variable and emits a success
event every second and an error
event if count
is divisible by 4
. When count
reaches 10, an end
event is emitted. This pattern allows the streaming of results as they arrive instead of waiting until the entire operation is completed.emitCount()
function:const counter = emitCount();
counter.on('success', (count) => {
console.log(`Count is: ${count}`);
});
counter.on('error', (err) => {
console.error(err.message);
});
counter.on('end', () => {
console.info('Counter has ended');
});
error
event is a special case in Node.js because, if there is no listener for it, the Node.js process will crash. You can comment out the error
event listener above and run the program to see what happens.Error
object is usually not precise enough to communicate all the different error types. Therefore, it is necessary to create custom error classes to better reflect the types of errors that could occur in your application. For example, you could have a ValidationError
class for errors that occur while validating user input, DatabaseError
class for database operations, TimeoutError
for operations that elapse their assigned timeouts, and so on.Error
object will retain the basic error properties, such as message
, name
, and stack
, but they can also have properties of their own. For example, a ValidationError
can be enhanced by adding meaningful properties, such as the portion of the input that caused the error. Essentially, you should include enough information for the error handler to properly handle the error or construct its own error messages.Error
object in Node.js:class ApplicationError extends Error {
constructor(message) {
super(message);
// name is set to the name of the class
this.name = this.constructor.name;
}
}
class ValidationError extends ApplicationError {
constructor(message, cause) {
super(message);
this.cause = cause
}
}
ApplicationError
class above is a generic error for the application, while the ValidationError
class represents any error that occurs when validating user input. It inherits from the ApplicationError
class and augments it with a cause
property to specify the input that triggered the error. You can use custom errors in your code just like you would with a normal error. For example, you can throw
it:function validateInput(input) {
if (!input) {
throw new ValidationError('Only truthy inputs allowed', input);
}
return input;
}
try {
validateInput(userJson);
} catch (err) {
if (err instanceof ValidationError) {
console.error(`Validation error: ${err.message}, caused by: ${err.cause}`);
return;
}
console.error(`Other error: ${err.message}`);
}
instanceof
keyword should be used to check for the specific error type, as shown above. Don't use the name of the error to check for the type, as in err.name === 'ValidationError'
, because it won't work if the error is derived from a subclass of ValidationError
.Retry-After
header does not exist, you need to delay the follow-up request and progressively increase the delay for each consecutive retry. This is known as the exponential back-off strategy. You also need to decide the maximum delay interval and how many times to retry the request before giving up. At that point, you should inform the caller that the target service is unavailable.undefined
is not a function", syntax errors, or reference errors should no longer exist in your codebase. Thankfully, this is not as daunting as it sounds. Migrating your entire Node.js application to TypeScript can be done incrementally so that you can start reaping the rewards immediately in crucial parts of the codebase. You can also adopt a tool like ts-migrate if you intend to perform the migration in one go.NaN
. When the failure is eventually noticed (usually after traveling through several other functions), it might be difficult to locate its origins.null
, undefined
, or -1
, when the problem can be handled locally. The former is the approach used by JSON.parse()
, which throws a SyntaxError
exception if the string to parse is not valid JSON, while the string.indexOf()
method is an example of the latter. Whichever you choose, make sure to document how the function deals with errors so that the caller knows what to expect.uncaughtException
event is emitted when an exception thrown somewhere in the application is not caught before it reaches the event loop. If an uncaught exception is detected, the application will crash immediately, but you can add a handler for this event to override this behavior. Indeed, many people use this as a last resort way to swallow the error so that the application can continue running as if nothing happened:// unsafe
process.on('uncaughtException', (err) => {
console.error(err);
});
uncaughtException
handler is to clean up any allocated resources, close connections, and log the error for later assessment before exiting the process.// better
process.on('uncaughtException', (err) => {
Honeybadger.notify(error); // log the error in a permanent storage
// attempt a gracefully shutdown
server.close(() => {
process.exit(1); // then exit
});
// If a graceful shutdown is not achieved after 1 second,
// shut down the process completely
setTimeout(() => {
process.abort(); // exit immediately and generate a core dump file
}, 1000).unref()
});
unhandledRejection
event is emitted when a rejected promise is not handled with a catch
block. Unlike uncaughtException
, these events do not cause the application to crash immediately. However, unhandled promise rejections have been deprecated and may terminate the process immediately in a future Node.js release. You can keep track of unhandled promise rejections through an unhandledRejection
event listener, as shown below:process.on('unhandledRejection', (reason, promise) => {
Honeybadger.notify({
message: 'Unhandled promise rejection',
params: {
promise,
reason,
},
});
server.close(() => {
process.exit(1);
});
setTimeout(() => {
process.abort();
}, 1000).unref()
});
systemd
or upstart
on Linux, and Docker users can use its restart policy. Once this is in place, reliable service will be restored almost instantly, and you'll still have the details of the uncaught exception so that it can be investigated and corrected promptly. You can go further by running more than one process and employ a load balancer to distribute incoming requests. This will help to prevent downtime in case one of the instances is lost temporarily.npm
to install the package:$ npm install @honeybadger-io/js --save
const Honeybadger = require('@honeybadger-io/js');
Honeybadger.configure({
apiKey: '[ YOUR API KEY HERE ]'
});
notify()
method, as shown in the following example:try {
// ...error producing code
} catch(error) {
Honeybadger.notify(error);
}
Error
class (or a subclass) should always be used to communicate errors in your code. Technically, you can throw
anything in JavaScript, not just Error
objects, but this is not recommended since it greatly reduces the usefulness of the error and makes error handling error prone. By consistently using Error
objects, you can reliably expect to access error.message
or error.stack
in places where the errors are being handled or logged. You can even augment the error class with other useful properties relevant to the context in which the error occurred.uncaughtException
or unhandledRejection
is detected. Don't try to recover from such errors!