Section 21: Advanced JavaScript - For Backend & Browser (Frontend)
The goals
💪🏻More on Functions & Working with Objects / Arrays
✌🏻Reference vs Primitive Values
👍🏻Asynchronous Code
When the function has a default value
Then we should write this parameter as the last
function greetUser(greetingPrefix, userName = 'user') {
console.log(greetingPrefix + ' ' + userName + '!');
}
greetUser('Hi', 'Max');
greetUser('Hello');
Special Values / Value Types in JavaScript
Rest Parameters & Spread Operator in JavaScript
function sumUp(...numbers) {
let result = 0;
for (const number of numbers) {
result += number; // result = result + number
}
return result;
}
const inputNumbers = [1, 5, 10, 11, 20, 31];
console.log(sumUp(...inputNumbers));
Then result on console is 78
which is 1 + 5 + 10 + 11 + 20 + 31
Since we use here to remove the square bracket, it would be no more array so we can just execute sum up function
Spread syntax (...)
Spread syntax (...) 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. In an object literal, the spread syntax enumerates the properties of an object and adds the key-value pairs to the object being created.
Spread syntax looks exactly like rest syntax. In a way, spread syntax is the opposite of rest syntax. Spread syntax "expands" an array into its elements, while rest syntax collects multiple elements and "condenses" them into a single element.
Syntax
myFunction(a, ...iterableObj, b)
[1, ...iterableObj, '4', 'five', 6]
{ ...obj, key: 'value' }
Functions are object!
In JavaScript, functions are first-class objects, because they can have properties and methods just like any other object. What distinguishes them from other objects is that functions can be called. In brief, they are Function objects.
- Primitives (string, number, null, boolean, undefined, symbol): these are immutable data types. They are not objects, don't have methods and they are stored in memory by value.
- Non-Primitives = Reference values (functions, arrays and objects): these are mutable data types. They are objects and they are stored in memory by reference.
As you can see, functions are inside the non-primitive category, which means that when you define a function you are creating an object.
// Function declaration.
function showFavoriteIceCream() {
const favIceCream = 'chocolate';
console.log(`My favorite ice cream is ${favIceCream}`);
}
// Let's assign a property.
showFavoriteIceCream.flavours = ['chocolate', 'vanilla', 'strawberry'];
// Let's log the showFavoriteIceCream function.
console.log(showFavoriteIceCream);
// Log
// { [Function: showFavoriteIceCream]
// flavours: [ 'chocolate', 'vanilla', 'strawberry' ] } -> property assigned
Here's what we get:
Let's assign a function and log it:
// Function declaration.
function showFavoriteIceCream() {
const favIceCream = 'chocolate';
console.log(`My favorite ice cream is ${favIceCream}`);
}
// Let's assign a property.
showFavoriteIceCream.flavours = ['chocolate', 'vanilla', 'strawberry'];
// Let's assign a function.
showFavoriteIceCream.showFlavours = function () {
return this.flavours;
};
// Let's log the showFavoriteIceCream function.
console.log(showFavoriteIceCream);
// Log
// { [Function: showFavoriteIceCream]
// flavours: [ 'chocolate', 'vanilla', 'strawberry' ],
// showFlavours: [Function] } -> function assigned
Here's what this produces:
As you can see, the showFavoriteIceCream function besides performing an action, is also behaving as an object, we are able to assign properties and methods to it.
Actually, function are known as first-class objects (or first-class citizens) which means that they can do more than that:
They can be stored in variables.
// stored in a variable. (Function expression)
const showFavIceCreams = function () {};
// stored in an array.
const listOfData = ['vanilla', 5, showFavIceCreams];
// stored in object.
const thisIsAnObject = {
showListOfIceCreams: function () {},
};
Preview
They can be passed as parameters in another function.
// Function declaration
function getFavoriteIceCream() {
return 'chocolate';
}
// Function declaration with params
function logFavoriteIceCream(func) {
return func();
}
// Passing getFavoriteIceCream as a parameter in logFavoriteIceCream
console.log(logFavoriteIceCream(getFavoriteIceCream)); // chocolate
Preview
They can return from another function.
// Function declaration
function getFavoriteIceCream() {
const myFavIceCream = 'chocolate';
// Returns another function declaration
return function () {
return 'My favorite ice cream is ' + myFavIceCream;
};
}
// storing function returned
const functionReturned = getFavoriteIceCream();
// executing function returned
console.log(functionReturned()); // My favorite ice cream is chocolate
Preview
Usage of backtick and dollar sign ($) in JavaScipt
Different Usage of Spread Operator
const person = { age: 32 };
function getAdultYears(p) {
p.age -= 18;
return p.age;
// return p.age - 18;
}
console.log(getAdultYears({ ...person })); // 14
console.log(person); // {age: 32}
Usually we know Spread operator works to pull out all the items in array to create a list of value.
But when we use it on an object, it pulls out all the key valus pairs off the object and gives us a list of those key value pairs.
So here, it brings out {age: 32}
Try and Catch to handle errors
Would it always work like if there are some error occur, then all the code stops the execution?
The answer is no.
Still we need to make it work regardless of errors.
In this case, we can use 'try' and 'catch' like below:
const fs = require('fs');
function readFile() {
try {
const fileData = fs.readFileSync('data.json');
} catch {
console.log('An error occurred!');
}
console.log('Hi there!');
}
readFile();
The try...catch statement is comprised of a try block and either a catch block, a finally block, or both. The code in the try block is executed first, and if it throws an exception, the code in the catch block will be executed. The code in the finally block will always be executed before control flow exits the entire construct.
Then why don't we wrap our all code with try-catch?
1. If we wrap everything, we will also catch a couple of errors that should indeed crash our application because they are simply bugs which we can fix during development.
2. Different errors that stem from different sources often should be handled in different ways
Error object
When an error occurs, JavaScript generates an object containing the details about it. The object is then passed as an argument to catch:
try {
// ...
} catch (err) { // <-- the "error object", could use another word instead of err
// ...
}
For all built-in errors, the error object has two main properties:
nameError name. For instance, for an undefined variable that’s "ReferenceError".messageTextual message about error details.
There are other non-standard properties available in most environments. One of most widely used and supported is:
stackCurrent call stack: a string with information about the sequence of nested calls that led to the error. Used for debugging purposes.
For instance:
try {
lalala; // error, variable is not defined!
} catch (err) {
alert(err.name); // ReferenceError
alert(err.message); // lalala is not defined
alert(err.stack); // ReferenceError: lalala is not defined at (...call stack)
// Can also show an error as a whole
// The error is converted to string as "name: message"
alert(err); // ReferenceError: lalala is not defined
}
Using “try…catch”
Let’s explore a real-life use case of try...catch.
As we already know, JavaScript supports the JSON.parse(str) method to read JSON-encoded values.
Usually it’s used to decode data received over the network, from the server or another source.
We receive it and call JSON.parse like this:
let json = '{"name":"John", "age": 30}'; // data from the server
let user = JSON.parse(json); // convert the text representation to JS object
// now user is an object with properties from the string
alert( user.name ); // John
alert( user.age ); // 30
If json is malformed, JSON.parse generates an error, so the script “dies”.
Should we be satisfied with that? Of course not!
This way, if something’s wrong with the data, the visitor will never know that (unless they open the developer console). And people really don’t like when something “just dies” without any error message.
Let’s use try...catch to handle the error:
let json = "{ bad json }";
try {
let user = JSON.parse(json); // <-- when an error occurs...
alert(user.name); // doesn't work
} catch (err) {
// ...the execution jumps here
alert("Our apologies, the data has errors, we'll try to request it one more time.");
alert(err.name);
alert(err.message);
}
Here we use the catch block only to show the message, but we can do much more: send a new network request, suggest an alternative to the visitor, send information about the error to a logging facility, … . All much better than just dying.
Throwing our own errors
What if json is syntactically correct, but doesn’t have a required name property?
Like this:
let json = '{ "age": 30 }'; // incomplete data
try {
let user = JSON.parse(json); // <-- no errors
alert( user.name ); // no name!
} catch (err) {
alert( "doesn't execute" );
}
Here JSON.parse runs normally, but the absence of name is actually an error for us.
To unify error handling, we’ll use the throw operator.
“Throw” operator
The throw operator generates an error.
The syntax is:
throw <error object>
Technically, we can use anything as an error object. That may be even a primitive, like a number or a string, but it’s better to use objects, preferably with name and message properties (to stay somewhat compatible with built-in errors).
JavaScript has many built-in constructors for standard errors: Error, SyntaxError, ReferenceError, TypeError and others. We can use them to create error objects as well.
Their syntax is:
let error = new Error(message);
// or
let error = new SyntaxError(message);
let error = new ReferenceError(message);
// ...
For built-in errors (not for any objects, just for errors), the name property is exactly the name of the constructor. And message is taken from the argument.
For instance:
let error = new Error("Things happen o_O");
alert(error.name); // Error
alert(error.message); // Things happen o_O
Let’s see what kind of error JSON.parse generates:
try {
JSON.parse("{ bad json o_O }");
} catch (err) {
alert(err.name); // SyntaxError
alert(err.message); // Unexpected token b in JSON at position 2
}
As we can see, that’s a SyntaxError.
And in our case, the absence of name is an error, as users must have a name.
So let’s throw it:
let json = '{ "age": 30 }'; // incomplete data
try {
let user = JSON.parse(json); // <-- no errors
if (!user.name) {
throw new SyntaxError("Incomplete data: no name"); // (*)
}
alert( user.name );
} catch (err) {
alert( "JSON Error: " + err.message ); // JSON Error: Incomplete data: no name
}
In the line (*), the throw operator generates a SyntaxError with the given message, the same way as JavaScript would generate it itself. The execution of try immediately stops and the control flow jumps into catch.
Now catch became a single place for all error handling: both for JSON.parse and other cases.
오류가 발생하면 항상 해당 오류에 대한 추가 정보(예: 오류로 이어지는 메시지 및 일련의 단계)가 포함된 일부 데이터(일반적으로 객체)를 얻습니다.
다음과 같이 해당 객체/데이터를 확보할 수 있습니다.
try {
somethingThatMightFail();
} catch (error) { // accept an "error parameter" after catch
console.log(error.message);
}
catch 후에 오류 데이터를 매개변수처럼 받아들일 수 있습니다(엄밀히 말하자면 함수가 아님에도 불구하고 말이죠).
그 데이터가 정확히 무엇인지(예: message 속성이 있는 객체)는 오류를 일으킨 함수/메소드에 따라 다릅니다.
여러분의 오류를 던질 수도 있습니다.
function doSomething() {
// do something ...
throw { message: 'Something went wrong! };
}
이건 조금 더 발전되었지만 결국 오류를 일으키는 경우에 이러한 모든 내장 함수와 메서드가 하는 일입니다.
Scoping & Shadowing in JavaScript
Scoping variables & constants
In JavaScript, a variable has two types of scope:
- Global Scope
- Local Scope
Global Scope
A variable declared at the top of a program or outside of a function is considered a global scope variable.
Let's see an example of a global scope variable.
// program to print a text
let a = "hello";
function greet () {
console.log(a);
}
greet(); // hello
In the above program, variable a is declared at the top of a program and is a global variable. It means the variable a can be used anywhere in the program.
The value of a global variable can be changed inside a function. For example,
// program to show the change in global variable
let a = "hello";
function greet() {
a = 3;
}
// before the function call
console.log(a);
//after the function call
greet();
console.log(a); // 3
In the above program, variable a is a global variable. The value of a is hello. Then the variable a is accessed inside a function and the value changes to 3.
Hence, the value of a changes after changing it inside the function.
Note: It is a good practice to avoid using global variables because the value of a global variable can change in different areas in the program. It can introduce unknown results in the program.
Local Scope
A variable can also have a local scope, i.e it can only be accessed within a function.
Example 1: Local Scope Variable
// program showing local scope of a variable
let a = "hello";
function greet() {
let b = "World"
console.log(a + b);
}
greet();
console.log(a + b); // error
Output
helloWorld
Uncaught ReferenceError: b is not defined
In the above program, variable a is a global variable and variable b is a local variable. The variable b can be accessed only inside the function greet. Hence, when we try to access variable b outside of the function, an error occurs.
let is Block Scoped
The let keyword is block-scoped (variable can be accessed only in the immediate block).
Example 2: block-scoped Variable
// program showing block-scoped concept
// global variable
let a = 'Hello';
function greet() {
// local variable
let b = 'World';
console.log(a + ' ' + b);
if (b == 'World') {
// block-scoped variable
let c = 'hello';
console.log(a + ' ' + b + ' ' + c);
}
// variable c cannot be accessed here
console.log(a + ' ' + b + ' ' + c);
}
greet();
Output
Hello World
Hello World hello
Uncaught ReferenceError: c is not defined
In the above program, variable
- a is a global variable. It can be accessed anywhere in the program.
- b is a local variable. It can be accessed only inside the function greet.
- c is a block-scoped variable. It can be accessed only inside the if statement block.
Hence, in the above program, the first two console.log() work without any issue.
However, we are trying to access the block-scoped variable c outside of the block in the third console.log(). This will throw an error.
Shadowing variables & constants
What is ‘variable shadowing’? Variable shadowing occurs when a variable of an inner scope is defined with the same name as a variable in the outer scope. In the inner scope, both variables’ scope overlap.

According to variable scoping rules, the inner scope should always be able to access a variable defined in the outer scope, but in practice, shadowing will prevent that from happening.
let points = 10;
function displayDouble() {
let number = 2;
number = points * number; // variable 'points' is accessible in the inner scope
console.log(number); //=> 20
}
displayDouble();
In this example, each variable is distinctly named and we can see that variable points initialised on line 1 in the outer scope is accessible in the inner scope of function .displayDouble(). This follows the principle we talked about where the variable in the outer scope is accessible in the inner scope.
Constructor function or class
A constructor is a special function that creates and initializes an object instance of a class. In JavaScript, a constructor gets called when an object is created using the new keyword.
The purpose of a constructor is to create a new object and set values for any existing object properties.
What Happens When A Constructor Gets Called?
When a constructor gets invoked in JavaScript, the following sequence of operations take place:
- A new empty object gets created.
- The this keyword begins to refer to the new object and it becomes the current instance object.
- The new object is then returned as the return value of the constructor.
JavaScript Constructor Examples
Here’s a few examples of constructors in JavaScript:
Using the "this" Keyword
When the this keyword is used in a constructor, it refers to the newly created object:
//Constructor
function User() {
this.name = 'Bob';
}
var user = new User();
Create Multiple Objects
In JavaScript, multiple objects can be created in a constructor:
//Constructor
function User() {
this.name = 'Bob';
}
var user1 = new User();
var user2 = new User();
In the above example, two objects are created using the same constructor.
Constructor with Parameters
A constructor can also have parameters:
//Constructor
function User (name, age) {
this.name = name;
this.age = age;
}
var user1 = new User('Bob', 25);
var user2 = new User('Alice', 27);
In the above example, arguments are passed to the constructor during object creation. This allows each object to have different property values.
Constructor vs Object Literal
An object literal is typically used to create a single object whereas a constructor is useful for creating multiple objects:
//Object literal
let user = {
name: 'Bob'
}
//Constructor
function User() {
this.name = 'Bob';
}
var user1 = new User();
var user2 = new User();
Each object created using a constructor is unique. Properties can be added or removed from an object without affecting another one created using the same constructor. However, if an object is built using an object literal, any changes made to the variable that is assigned the object value will change the original object.
Object Prototype
Properties and methods can be added to a constructor using a prototype:
//Constructor
function User() {
this.name = 'Bob';
}
let user1 = new User();
let user2 = new User();
//Adding property to constructor using prototype
User.prototype.age = 25;
console.log(user1.age); // 25
console.log(user2.age); // 25
In the above example, two User objects are created using the constructor. A new property age is later added to the constructor using a prototype, which is shared across all instances of the User object.
class Job {
constructor(jobTitle, place, salary) {
this.title = jobTitle;
this.location = place;
this.salary = salary;
}
describe() {
console.log(
`I'm a ${this.title}, I work in ${this.location} and I earn ${this.salary}.`
);
}
}
const developer = new Job('Developer', 'New York', 50000);
const cook = new Job('Cook', 'Munich', 35000);
developer.describe(); // I'm a Developer, I work in New York and I earn 50000.
cook.describe(); // I'm a Cook, I work in Munich and I earn 35000.
Here, 'this' means the object based on class
Array & Object destructuring
e.g.
const { v4: uuidv4 } = require('uuid');
uuidv4();
What is Destructuring in JavaScript?
Imagine you want to pick out some shoes from your collection, and you want your favorite blue ones. The very first thing you have to do is to search through your collection and unpack whatever you have there.
Now destructuring is just like that approach you took when looking for your shoes. Destructuring is the act of unpacking elements in an array or object.
Destructuring not only allow us to unpack elements, it also gives you the power to manipulate and switch elements you unpacked depending on the type of operation you want to perform.
Destructuring in Arrays
To destructure an array in JavaScript, we use the square brackets [] to store the variable name which will be assigned to the name of the array storing the element.
const [var1, var2, ...] = arrayName;
The dots right after the var2 declared above simply means that we can create more variables depending on how many items we want to remove from the array.
Destructuring in Objects
When destructuring objects, we use curly braces with the exact name of what we have in the object. Unlike in arrays where we can use any variable name to unpack the element, objects allow just the use of the name of the stored data.
Interestingly, we can manipulate and assign a variable name to the data we want to get from the object. Let's see all of that now in code.
const freeCodeCamp = {
frontend: "React",
backend: "Node",
database: "MongoDB",
};
const { frontend, backend } = freeCodeCamp;
console.log(frontend, backend);
// React, Node
Logging what we have to the console shows the value of frontend and backend. Let's now see how to assign a variable name to the object we want to unpack.
const freeCodeCamp = {
frontend: "React",
backend: "Node",
database: "MongoDB",
};
const { frontend: courseOne, backend: courseTwo } = freeCodeCamp;
console.log(courseOne, courseTwo);
// React, Node
As you can see, we have courseOne and courseTwo as the names of the data we want to unpack.
Assigning a variable name will always help us keep our code clean, especially when it comes to working with external data when we want to unpack and reuse it across our code.
Asynchronous Code
const fs = require('fs');
function readFile() {
let fileData = fs.readFileSync('data.txt');
console.log(fileData);
console.log('Hi there!');
}
readFile();
Imagine if we need to bring some data from a different file.
It can be just small amount of data but if it's a huge data then it will take some time to load and all our script needs to wait for it. It's quite inefficient.
So, in this case, we can use 'asynchronous code'.
const fs = require('fs');
function readFile() {
let fileData;
fs.readFile('data.txt', function(error, fileData) {
console.log('File parsing done!');
console.log(fileData.toString());
});
console.log('Hi there!');
}
readFile();
// result
// Hi there!
// File parsing done!
// This works - data from the text file!
fs.readFile(path[, options], callback)
Asynchronously reads the entire contents of a file.
import { readFile } from 'node:fs';
readFile('/etc/passwd', (err, data) => {
if (err) throw err;
console.log(data);
});
The callback is passed two arguments (err, data), where data is the contents of the file.
If no encoding is specified, then the raw buffer is returned.
If options is a string, then it specifies the encoding:
import { readFile } from 'node:fs';
readFile('/etc/passwd', 'utf8', callback);
Promise
const fs = require('fs/promises');
function readFile() {
let fileData;
// fs.readFile('data.txt', function(error, fileData) {
// console.log('File parsing done!');
// console.log(fileData.toString());
// // start another async task that sends the data to a database
// });
fs.readFile('data.txt')
.then(function (fileData) {
console.log('File parsing done!');
console.log(fileData.toString());
// return anotherAsyncOperation;
})
.then(function () {});
console.log('Hi there!');
}
readFile();
Error handling with promises
Promise chains are great at error handling. When a promise rejects, the control jumps to the closest rejection handler. That’s very convenient in practice.
For instance, in the code below the URL to fetch is wrong (no such site) and .catch handles the error:
fetch('https://no-such-server.blabla') // rejects
.then(response => response.json())
.catch(err => alert(err)) // TypeError: failed to fetch (the text may vary)
As you can see, the .catch doesn’t have to be immediate. It may appear after one or maybe several .then.
Or, maybe, everything is all right with the site, but the response is not valid JSON. The easiest way to catch all errors is to append .catch to the end of chain:
fetch('/article/promise-chaining/user.json')
.then(response => response.json())
.then(user => fetch(`https://api.github.com/users/${user.name}`))
.then(response => response.json())
.then(githubUser => new Promise((resolve, reject) => {
let img = document.createElement('img');
img.src = githubUser.avatar_url;
img.className = "promise-avatar-example";
document.body.append(img);
setTimeout(() => {
img.remove();
resolve(githubUser);
}, 3000);
}))
.catch(error => alert(error.message));
Normally, such .catch doesn’t trigger at all. But if any of the promises above rejects (a network problem or invalid json or whatever), then it would catch it.
Can we use Try-Catch with Promise? - nope!
You cannot use try-catch statements to handle exceptions thrown asynchronously, as the function has "returned" before any exception is thrown. You should instead use the promise.then and promise.catch methods, which represent the asynchronous equivalent of the try-catch statement. (Or use the async/await syntax noted in @Edo's answer.)
What you need to do is to return the promise, then chain another .catch to it:
function promise() {
var promise = new Promise(function(resolve, reject) {
throw('Oh no!');
});
return promise.catch(function(error) {
throw(error);
});
}
promise().catch(function(error) {
console.log('Caught!', error);
});
Promises are chainable, so if a promise rethrows an error, it will be delegated down to the next .catch.
By the way, you don't need to use parentheses around throw statements (throw a is the same as throw(a)).
Async / Await
It makes the code look like a synchronous code even it's an ansynchronous code.
And inside it uses Promise.
We found the clear winner, didn't we? Observables own everything. Well, observables are amazing and I can only recommend using them. But if you're dealing with an issue where the operators don't help you (even though that's hard to imagine), you might not really face a disadvantage when using promises.
There's one thing which can be annoying when using both promises or observables: You're writing code "in block", not like you normally do. Wouldn't it be nice if you could write async code just like you write synchronous one? Like that?
auth = checkAuth() // <- async operation
user = getUser(auth) // <- async operation
console.log(user.name)
That would be nice but it's of course not possible due to the way JavaScript works. It doesn't wait for your asynchronous operations to finish, hence you can't mix them with your synchronous code like that. But with a new feature added by ES8, you suddenly can!
async function fetchUser() {
const auth = await checkAuth() // <- async operation
const user = await getUser(auth) // <- async operation
return user
}
fetchUser().then(user => console.log(user.name))
In the example, you see that we still use then() to react to the data passed by our promise. But before we do that, we actually use two promises (both checkAuth() as well as getUser() return a promise!) to fetch a user. Even though we work with promises here, we write code just like we write "normal", synchronous code. How does that work?
The magic happens via two keywords: async and await. You put async in front of a function to turn it into an async function. Such a function will in the end always resolve as a promise - even though it doesn't look like one. But you could rewrite the example like this.
function fetchUser() {
return checkAuth()
.then(auth => {
return getUser(auth)
})
.then(user => {
return user
})
}
fetchUser().then(user => console.log(user.name))
This also gives you a clue about what await does: It simply waits (awesome, right?) for a promise to resolve. It basically does the same then() does. It waits for the promise to resolve and then takes the value the promise resolved to and stores it into a variable. The code after await checkAuth() will only execute once it's done - it's like chaining then() calls therefore.
What about handling errors? That's pretty easy, too. Since we're now writing "synchronous" code again (we aren't), we can simply use try-catch again.
async function fetchUser() {
try {
const auth = await checkAuth() // <- async operation
const user = await getUser(auth) // <- async operation
return user
} catch (error) {
return { name: 'Default' }
}
}
fetchUser().then(user => console.log(user.name))
Async/ await gives you a powerful tool for working with promises. It doesn't work with observables though. The reason being that behind the scenes, async/ await really only uses promises.
Async / Await vs Promise
Thumb Rules for Using Promises
- Use promises whenever you are using asynchronous or blocking code.
- resolve maps to then and reject maps to catch for all practical purposes.
- Make sure to write both .catch and .then methods for all the promises.
- If something needs to be done in both cases use .finally.
- We only get one shot at mutating each promise.
- We can add multiple handlers to a single promise.
- The return type of all the methods in the Promise object, regardless of whether they are static methods or prototype methods, is again a Promise.
- In Promise.all, the order of the promises are maintained in the values variable, irrespective of which promise was first resolved.
Once you have wrapped your head around promises, check out async-await. It helps you to write code that is much more readable. When it is not used properly, it has its downsides.
Thumb Rules for async-await
Here’s a list of thumb rules that I use to stay sane while using async and await.
- async functions return a promise.
- async functions use an implicit Promise to return results. Even if you don’t return a promise explicitly, the async function makes sure that your code is passed through a promise.
- await blocks the code execution within the async function, of which it (await statement) is a part.
- There can be multiple await statements within a single async function.
- When using async await, make sure you use try catch for error handling.
- Be extra careful when using await within loops and iterators. You might fall into the trap of writing sequentially-executing code when it could have been easily done in parallel.
- await is always for a single Promise.
- Promise creation starts the execution of asynchronous functionality.
- await only blocks the code execution within the async function. It only makes sure that the next line is executed when the promise resolves. So, if an asynchronous activity has already started, await will not have any effect on it.
Should I Use Promises or async-await
The answer is that we will use both.
Here are the thumb rules that I use to decide when to use promises and when to use async-await.
- The async function returns a promise. The converse is also true. Every function that returns a promise can be considered as async function.
- await is used for calling an async function and waits for it to resolve or reject.
- await blocks the execution of the code within the async function in which it is located.
- If the output of function2 is dependent on the output of function1, I use await.
- If two functions can be run in parallel, create two different async functions and then run them in parallel.
- To run promises in parallel, create an array of promises and then use Promise.all(promisesArray).
- Every time you use await remember that you are writing blocking code. Over time we tend to neglect this.
- Instead of creating huge async functions with many await asyncFunction() in it, it is better to create smaller async functions. This way, we will be aware of not writing too much blocking code.
- Another advantage of using smaller async functions is that you force yourself to think of which async functions can be run in parallel.
- If your code contains blocking code, it is better to make it an async function. By doing this, you are making sure that somebody else can use your function asynchronously.
- By making async functions out of blocking code, you are enabling the user (who will call your function) to decide on the level of asynchronicity they want.
Sources
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax
Spread syntax (...) - JavaScript | MDN
Spread syntax (...) 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. In an object literal, the spread syntax enumerates the properties
developer.mozilla.org
https://academind.com/tutorials/javascript-functions-are-objects
JS Functions Are Objects
Unknown to many JavaScript developers, functions are objects in JS. This matters and has important implications which are discussed here.
academind.com
https://javascript.info/try-catch
Error handling, "try...catch"
javascript.info
https://www.programiz.com/javascript/variable-scope
JavaScript Variable Scope (with Examples)
JavaScript Variable Scope In this tutorial, you will learn about variable scope in JavaScript with the help of examples. Scope refers to the availability of variables and functions in certain parts of the code. In JavaScript, a variable has two types of sc
www.programiz.com
https://mayuminishimoto.medium.com/understanding-variable-shadowing-with-javascript-58fc108c8f03
Understanding ‘Variable Shadowing’ with JavaScript
What is ‘variable shadowing’ and why it should be avoided.
mayuminishimoto.medium.com
JavaScript Constructors: What You Need to Know
JavaScript constructors are special functions that creates and initializes an object instance of a class when created using the "new" keyword.
rollbar.com
https://www.w3schools.com/js/js_asynchronous.asp
JavaScript Asynchronous
W3Schools offers free online tutorials, references and exercises in all the major languages of the web. Covering popular subjects like HTML, CSS, JavaScript, Python, SQL, Java, and many, many more.
www.w3schools.com
https://nodejs.org/api/fs.html#fsreadfilepath-options-callback
File system | Node.js v18.8.0 Documentation
nodejs.org
https://javascript.info/promise-error-handling
Error handling with promises
javascript.info
https://stackoverflow.com/questions/24977516/catching-errors-in-javascript-promises-with-a-first-level-try-catch
Catching Errors in JavaScript Promises with a First Level try ... catch
So, I want my first level catch to be the one that handles the error. Is there anyway to propagate my error up to that first catch? Reference code, not working (yet): Promise = require('./framework/
stackoverflow.com
https://academind.com/tutorials/callbacks-vs-promises-vs-rxjs-vs-async-awaits
Callbacks vs Promises vs RxJS vs async/await
You handle async code all the time in JavaScript. You're probably doing it wrong. 3 ways of doing it right.
academind.com
https://betterprogramming.pub/understanding-promises-in-javascript-13d99df067c1
Understanding Promises in JavaScript
An in-depth look at creating and handling Promises
betterprogramming.pub
https://betterprogramming.pub/should-i-use-promises-or-async-await-126ab5c98789
JavaScript: Promises or async-await
A set of rules for when to use which
betterprogramming.pub