Mastering JavaScript: Async JS & Browser APIs

Asynchronous programming in JavaScript allows you to perform long-running operations without blocking the main thread, ensuring your applications remain responsive. This guide covers the key concepts and techniques for handling asynchronous operations, including setTimeout, setInterval, callbacks, promises, and async/await. We'll also dive into working with APIs using XMLHttpRequest and the Fetch API.
Asynchronous JavaScript
setTimeout
The setTimeout function executes a function after a specified delay.
Syntax:
setTimeout(function, delay, ...args)
function: The function to execute.delay: Time in milliseconds before the function is executed.args: Additional arguments to pass to the function.
Example:
setTimeout(() => {
console.log('Hello after 2 seconds');
}, 2000);
Example with arguments:
setTimeout((message) => {
console.log(message);
}, 2000, 'Hello after 2 seconds');
setInterval
The setInterval function repeatedly executes a function at specified intervals.
Syntax:
setInterval(function, interval, ...args)
function: The function to execute.interval: Time in milliseconds between each execution.args: Additional arguments to pass to the function.
Example:
setInterval(() => {
console.log('Hello every 2 seconds');
}, 2000);
Example with arguments:
setInterval((message) => {
console.log(message);
}, 2000, 'Hello every 2 seconds');
Clearing Timers
To stop a setTimeout or setInterval, use clearTimeout or clearInterval.
Example:
const timeoutId = setTimeout(() => {
console.log('This will not run');
}, 2000);
clearTimeout(timeoutId);
const intervalId = setInterval(() => {
console.log('This will not run');
}, 2000);
clearInterval(intervalId);
Example with named function:
function greet() {
console.log('This will not run');
}
const timeoutId = setTimeout(greet, 2000);
clearTimeout(timeoutId);
const intervalId = setInterval(greet, 2000);
clearInterval(intervalId);
Callbacks
Callbacks are functions passed as arguments to other functions and executed after some operation completes.
Example:
function fetchData(callback) {
setTimeout(() => {
callback('Data fetched');
}, 2000);
}
fetchData(data => {
console.log(data); // "Data fetched" after 2 seconds
});
Example with error handling
function fetchData(callback) {
setTimeout(() => {
if (Math.random() < 0.5) {
callback(null, 'Data fetched');
} else {
callback('Error fetching data', null);
}
}, 2000);
}
fetchData((error, data) => {
if (error) {
console.error(error);
} else {
console.log(data); // "Data fetched" after 2 seconds
}
});
Callback Hell
Callback hell occurs when callbacks are nested within other callbacks, making the code hard to read and maintain.
Example:
fetchData(data1 => {
console.log(data1);
fetchData(data2 => {
console.log(data2);
fetchData(data3 => {
console.log(data3);
// And so on...
});
});
});

Promises
Promises provide a cleaner way to handle asynchronous operations, avoiding callback hell.
Syntax:
const promise = new Promise((resolve, reject) => {
// Asynchronous operation
});
Example:
const fetchData = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Data fetched');
}, 2000);
});
};
fetchData()
.then(data => {
console.log(data); // "Data fetched" after 2 seconds
})
.catch(error => {
console.error(error);
});
Example with both resolve and reject
const fetchData = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (Math.random() < 0.5) {
resolve('Data fetched');
} else {
reject('Error fetching data');
}
}, 2000);
});
};
fetchData()
.then(data => {
console.log(data); // "Data fetched" after 2 seconds
})
.catch(error => {
console.error(error); // "Error fetching data"
});
Chaining Promises
You can chain promises to handle multiple asynchronous operations in sequence.
Example:
const fetchData1 = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Data 1 fetched');
}, 1000);
});
};
const fetchData2 = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Data 2 fetched');
}, 1000);
});
};
fetchData1()
.then(data1 => {
console.log(data1);
return fetchData2();
})
.then(data2 => {
console.log(data2);
})
.catch(error => {
console.error(error);
});
async/await
async/await provides a syntax for writing asynchronous code in a synchronous style, making it easier to read and maintain.
Example:
const fetchData = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Data fetched');
}, 2000);
});
};
const getData = async () => {
try {
const data = await fetchData();
console.log(data); // "Data fetched" after 2 seconds
} catch (error) {
console.error(error);
}
};
getData();
Example with both resolve and reject
const fetchData = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (Math.random() < 0.5) {
resolve('Data fetched');
} else {
reject('Error fetching data');
}
}, 2000);
});
};
const getData = async () => {
try {
const data = await fetchData();
console.log(data); // "Data fetched" after 2 seconds
} catch (error) {
console.error(error); // "Error fetching data"
}
};
getData();

Handling Multiple Promises
Using Promise.all, you can handle multiple promises in parallel.
Example:
const fetchData1 = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Data 1 fetched');
}, 1000);
});
};
const fetchData2 = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Data 2 fetched');
}, 1000);
});
};
const getData = async () => {
try {
const results = await Promise.all([fetchData1(), fetchData2()]);
console.log(results); // ["Data 1 fetched", "Data 2 fetched"]
} catch (error) {
console.error(error);
}
};
getData();
Using Promise.race
Promise.race returns the first promise that settles (either resolves or rejects).
Example:
const fetchData1 = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Data 1 fetched');
}, 1000);
});
};
const fetchData2 = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Data 2 fetched');
}, 500);
});
};
Promise.race([fetchData1(), fetchData2()])
.then(data => {
console.log(data); // "Data 2 fetched" after 500 ms
})
.catch(error => {
console.error(error);
});
Using Promise.allSettled
Promise.allSettled returns a promise that resolves after all of the given promises have either resolved or rejected.
Example:
const fetchData1 = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Data 1 fetched');
}, 1000);
});
};
const fetchData2 = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject('Error fetching data 2');
}, 500);
});
};
Promise.allSettled([fetchData1(), fetchData2()])
.then(results => {
results.forEach((result) => {
if (result.status === 'fulfilled') {
console.log(result.value);
} else {
console.error(result.reason);
}
});
});
Working with APIs
APIs (Application Programming Interfaces) allow you to interact with external services or data sources. There are several ways to make HTTP requests in JavaScript.
XMLHttpRequest
XMLHttpRequest is an older way to make HTTP requests.
It provides a way to interact with servers using a browser's built-in XMLHttpRequest object.
Example:
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.example.com/data', true);
xhr.onload = function() {
if (xhr.status === 200) {
console.log(xhr.responseText);
} else {
console.error('Request failed');
}
};
xhr.send();
Example with error handling:
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.example.com/data', true);
xhr.onload = function() {
if (xhr.status >= 200 && xhr.status < 300) {
console.log(xhr.responseText);
} else {
console.error('Request failed with status', xhr.status);
}
};
xhr.onerror = function() {
console.error('Network error');
};
xhr.send();
Fetch
The Fetch API provides a modern way to make HTTP requests. It returns promises, making it easier to handle asynchronous operations.
Example:
fetch('https://api.example.com/data')
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
console.log(data);
})
.catch(error => {
console.error('There has been a problem with your fetch operation:', error);
});
Example with different HTTP methods:
// GET request
fetch('https://api.example.com/data')
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
console.log(data);
})
.catch(error => {
console.error('There has been a problem with your fetch operation:', error);
});
// POST request
fetch('https://api.example.com/data', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ key: 'value' }),
})
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
console.log(data);
})
.catch(error => {
console.error('There has been a problem with your fetch operation:', error);
});
Using async/await with Fetch
Example:
const fetchData = async () => {
try {
const response = await fetch('https://api.example.com/data');
if (!response.ok) {
throw new Error('Network response was not ok');
}
const data = await response.json();
console.log(data);
} catch (error) {
console.error('There has been a problem with your fetch operation:', error);
}
};
fetchData();
Handling Errors
When making HTTP requests, it's important to handle errors gracefully.
Example with Fetch:
const fetchData = async () => {
try {
const response = await fetch('https://api.example.com/data');
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const data = await response.json();
console.log(data);
} catch (error) {
console.error('There has been a problem with your fetch operation:', error);
}
};
fetchData();
Sending Data
You can send data using the Fetch API by specifying the request method and providing the data in the request body.
Example:
const sendData = async (data) => {
try {
const response = await fetch('https://api.example.com/data', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
if (!response.ok) {
throw new Error('Network response was not ok');
}
const result = await response.json();
console.log(result);
} catch (error) {
console.error('There has been a problem with your fetch operation:', error);
}
};
sendData({ key: 'value' });
Conclusion
Asynchronous JavaScript is essential for building responsive and efficient applications. This guide has covered various techniques, including setTimeout, setInterval, callbacks, promises, and async/await. Additionally, we explored working with APIs using XMLHttpRequest and the Fetch API. Understanding these concepts and how to handle asynchronous operations effectively will help you write better JavaScript code and build more dynamic web applications. By mastering these topics, you'll be well-equipped to handle the complexities of modern JavaScript development.
In the next article, we'll dive deeper into some advanced JavaScript concepts, such as Iterators and Generators, Classes, and Modules. These topics will further enhance your understanding of JavaScript and enable you to write cleaner, more efficient code. Stay tuned for an in-depth exploration of these powerful features!





