Blocks with try/catch are the tried-and-true means for capturing errors when things go wrong:
try {
someRiskyOperation();
catch (error) {
console.error(“Something’s gone terribly wrong”, error);
}
In this case, we log the error to the console with console.error
. You could choose to throw the error, passing it up to the next handler. Note that this breaks code flow execution; that is, the current execution stops and the next error handler up the stack takes over:
try {
someRiskyOperation();
catch (error) {
throw new Error(“Someone else deal with this.”, error);
}
Modern JavaScript offers quite a few useful properties on its Error
objects, including Error.stack
for getting a look at the stack trace. In the above example, we are setting the Error.message
property and Error.cause
with the constructor arguments.
Another place you’ll find errors is in asynchronous code blocks where you handle normal outcomes with .then()
. In this case, you can use an on(‘error’)
handler or onerror
event, depending on how the promise returns the errors. Sometimes, the API will give you back an error object as a second return value with the normal value. (If you use await on the async call, you can wrap it in a try/catch to handle any errors.) Here’s a simple example of handling an asynchronous error:
someAsyncOperation()
.then(result => {
// All is well
})
.catch(error => {
// Something’s wrong
console.error("Problems:", error);
});
No matter what, don’t ever swallow errors! I won't show that here because someone might copy and paste it. Basically, if you catch an error and then do nothing, your program will silently continue operating without any obvious indication that something went wrong. The logic will be broken and you’ll be left to ponder until you find your catch block with nothing in it. (Note, providing a finally{}
block without a catch block will cause your errors to be swallowed.)
JavaScript currying
Currying is a method of making functions more flexible. With a curried function, you can pass all of the arguments that the function is expecting and get the result, or you can pass only a subset of arguments and receive a function back that waits for the remainder of the arguments. Here's a simple example of a curry:
var myFirstCurry = function(word) {
return function(user) {
return [word , ", " , user].join("");
};
};
var HelloUser = myFirstCurry("Hello");
HelloUser("InfoWorld"); // Output: "Hello, InfoWorld"
The original curried function can be called directly by passing each of the parameters in a separate set of parentheses, one after the other:
myFirstCurry("Hey, how are you?")("InfoWorld"); // Output: "Hey, how are you?, InfoWorld"
This is an interesting technique that allows you to create function factories, where the outer functions let you partially configure the inner one. For example, you could also use the above curried function like so:
let greeter = myFirstCurry("Namaste");
greeter("InfoWorld"); // output: “Namaste, InfoWorld”
In real-world usage, this idea can be a help when you need to create many functions that vary according to certain parameters.
JavaScript apply, call, and bind methods
Although it’s not every day that we use them, it’s good to understand what the call
, apply
, and bind
methods are. Here, we are dealing with some serious language flexibility. At heart, these methods allow you to specify what the this
keyword resolves to.
In all three functions, the first argument is always the this
value, or context, that you want to give to the function.
Of the three, call
is the easiest. It's the same as invoking a function while specifying its context. Here’s an example:
var user = {
name: "Info World",
whatIsYourName: function() {
console.log(this.name);
}
};
user.whatIsYourName(); // Output: "Info World",
var user2 = {
name: "Hack Er"
};
user.whatIsYourName.call(user2); // Output: "Hack Er"
Note that apply
is nearly the same as call
. The only difference is that you pass arguments as an array and not separately. Arrays are easier to manipulate in JavaScript, opening a larger number of possibilities for working with functions. Here's an example using apply
and call
:
var user = {
greet: "Hello!",
greetUser: function(userName) {
console.log(this.greet + " " + userName);
}
};
var greet1 = {
greet: "Hola"
};
user.greetUser.call(greet1,"InfoWorld") // Output: "Hola InfoWorld"
user.greetUser.apply(greet1,["InfoWorld"]) // Output: "Hola InfoWorld"
The bind
method allows you to pass arguments to a function without invoking it. A new function is returned with arguments bounded preceding any further arguments. Here's an example:
var user = {
greet: "Hello!",
greetUser: function(userName) {
console.log(this.greet + " " + userName);
}
};
var greetHola = user.greetUser.bind({greet: "Hola"});
var greetBonjour = user.greetUser.bind({greet: "Bonjour"});
greetHola("InfoWorld") // Output: "Hola InfoWorld"
greetBonjour("InfoWorld") // Output: "Bonjour InfoWorld"
JavaScript memoization
Memoization is an optimization technique that speeds up function execution by storing results of expensive operations and returning the cached results when the same set of inputs occur again. JavaScript objects behave like associative arrays, making it easy to implement memoization in JavaScript. Here's how to convert a recursive factorial function into a memoized factorial function:
function memoizeFunction(func) {
var cache = {};
return function() {
var key = arguments[0];
if(cache[key]) {
return cache[key];
}
else {
var val = func.apply(this, arguments);
cache[key] = val;
return val;
}
};
}
var fibonacci = memoizeFunction(function(n) {
return (n === 0 || n === 1) ? n : fibonacci(n - 1) + fibonacci(n - 2);
});
JavaScript IIFE
An immediately invoked function expression (IIFE) is a function that is executed as soon as it is created. It has no connection with any events or asynchronous execution. You can define an IIFE as shown here:
(function() {
// all your code here
// ...
})();
The first pair of parentheses function(){...}
converts the code inside the parentheses into an expression.The second pair of parentheses calls the function resulting from the expression. An IIFE can also be described as a self-invoking anonymous function. Its most common usage is to limit the scope of a variable made via var
or to encapsulate context to avoid name collisions.
There are also situations where you need to call a function using await, but you're not inside an async function block. This happens sometimes in files that you want to be executable directly and also imported as a module. You can wrap such a function call in an IIFE block like so:
(async function() {
await callAsyncFunction()
})();
Useful argument features
Although JavaScript doesn’t support method overloading (because it can handle arbitrary argument counts on functions), it does have several powerful facilities for dealing with arguments. For one, you can define a function or method with default values:
function greet(name = 'Guest')
{
console.log(`Hello, ${name}!`);
}
greet(); // Outputs: Hello, Guest!
greet('Alice'); // Outputs: Hello, Alice!
You can also accept and handle all the arguments at once, so that you can handle any number of arguments passed in. This uses the rest
operator to collect all the arguments into an array:
function sum(...numbers) {
return numbers.reduce((total, num) => total + num, 0);
}
console.log(sum(1, 2, 3)); // Outputs: 6
console.log(sum(4, 5)); // Outputs: 9
If you really need to deal with differing argument configurations, you can always check them:
function findStudent(firstName, lastName) {
if (typeof firstName === 'string' && typeof lastName === 'string') {
// Find by first and last name
} else if (typeof firstName === 'string') {
// Find by first name
} else {
// Find all students
}
}
findStudent('Alice', 'Johnson'); // Find by first and last name
findStudent('Bob'); // Find by first name
findStudent(); // Find all
Also, remember that JavaScript includes a built-in arguments array. Every function or method automatically gives you the arguments
variable, holding all the arguments passed to the call.
Conclusion
As you become familiar with Node, you’ll notice there are many ways to solve almost every problem. The right approach isn't always obvious. Sometimes, there are several valid approaches to a given situation. Knowing about the many options available helps.
The 10 JavaScript concepts discussed here are basics every Node developer will benefit from knowing. But they're the tip of the iceberg. JavaScript is a powerful and complex language. The more you use it, the more you will understand how vast JavaScript really is, and how much you can do with it.