API Basics Lesoon Intro
Asynchronous Programming For APIs
A quick intro to JavaScript Asynchronous Programming
One of the most important concepts to understand in Javascript is how it handles synchronous and asynchronous code.
Default Behavior in Javascript is Synchronous
JavaScript runs code line by line, in order. This is known as synchronous execution.
Example of synchronous behavior:
console.log(“One”);
console.log(“Two”);
console.log(“Three”);
Output:
One
Two
Three
This works great — until you hit time-consuming tasks like:
- Fetching data from a server
- Reading files
- Waiting for timers
In these cases, if we use synchronous code it will block everything else until the task is done — and user experience will suffer.
Enter Asynchronous JavaScript
Asynchronous operations allow JavaScript to move on to other tasks while waiting for something (like a network request) to finish. JavaScript achieves this using tools like:
- setTimeout()
- async/await
- Promises
These let code keep running without getting “stuck” waiting.
Example that illustrates asynchronous behavior using setTimeout
Notice that output shows ‘after timeout’ first followed by ‘Timeout done’
This is because setTimeout is asynchronous and not blocking
%%js
setTimeout(() => {
console.log("Timeout done");
}, 2000);
console.log("After timeout");
<IPython.core.display.Javascript object>
Example that uses async/await functions to fetch data from a Web API without blocking
async and await:
- The async keyword makes a function asynchronous, and
- await pauses its execution until a Promise resolves,
When the script runs, the following sequence of events occurs:
- In main loop, fetchTodo() is called which invokes the function.
- The first console.log statement, “1. Calling fetch…”, is executed immediately from within the function
- The script reaches await fetch(…). This line initiates a network request and pauses fetchTodo()’s execution.
- However, the rest of the main script continues to run.
- The second console.log statement in main loop, “2. This line runs immediately…”, is executed.
- This demonstrates that the main program flow isn’t blocked while the network request is pending, which is the core concept of asynchronous programming.
- The network request completes. The fetchTodo() function resumes its execution, and the next await keyword processes the response as JSON.
- Finally, the last console.log statements inside the fetchTodo() function are executed, confirming that the data has been successfully fetched and processed.
The try…catch block handles any potential errors that might occur during the network request or data processing.
%%js
async function fetchTodo() {
try {
console.log('1. Calling fetch...');
// The await keyword pauses this function's execution here...
const response = await fetch('https://jsonplaceholder.typicode.com/todos/1');
if (!response.ok) {
throw new Error(`Error: ${response.status}`);
}
// ...and resumes here once the Promise is fulfilled.
const todo = await response.json();
console.log('3. Data fetched and processed.');
console.log(todo);
} catch (error) {
console.error('Failed to fetch to-do item:', error);
}
}
// This is the main part of the script that runs immediately
fetchTodo();
console.log('2. This line runs immediately, before the fetch completes!');
<IPython.core.display.Javascript object>
Promises
- The async/await syntax is just syntactic sugar for Promises and .then() chains.
- The JavaScript engine transforms async/await code into a series of Promise calls.
What is a Promise
- A Promise is a JavaScript object that represents a task that will be completed in the future.
- It’s a placeholder for the eventual result of an asynchronous operation, like fetching data or waiting for a timer to finish.
- A Promise can be in one of three states:
Pending: The initial state. The asynchronous task is still running.
Fulfilled: The task completed successfully, and the Promise has a resulting value.
Rejected: The task failed, and the Promise has an error.
%%js
// Simple example to illustrate Javascript Promises
console.log("A. Creating the promise...");
const myPromise = new Promise((resolve, reject) => {
console.log("B. The executor function runs immediately!");
setTimeout(() => {
resolve("C. The promise is resolved (after 2 seconds).");
}, 2000);
});
console.log("D. This code runs after the promise is created but before it's resolved.");
myPromise.then(message => {
console.log(message);
});
console.log("E. This code also runs after the promise is created but before it's resolved.");
<IPython.core.display.Javascript object>
The Output
The output in the console will be in this exact order, demonstrating the immediate execution of the promise’s constructor code:
A. Creating the promise…
B. The executor function runs immediately!
D. This code runs after the promise is created but before it’s resolved.
E. This code also runs after the promise is created but before it’s resolved.
(2-second delay)
C. The promise is resolved (after 2 seconds).
- When a promise is created, the code inside the executor function (the function passed to new Promise()) runs immediately and synchronously.
- It does not wait for the .then() consuming part.
- The asynchronous part begins when the executor function starts a task that takes time, like a network request or a setTimeout.
Here’s a breakdown:
- Creation Phase (Synchronous): The new Promise() constructor is called, and the code inside its executor function starts executing right away. This is where you would initiate your long-running task.
- Settling Phase (Asynchronous): The executor function, once it completes its asynchronous task, will eventually call either resolve() or reject(). This moves the promise from the pending state to fulfilled or rejected.
- Consumption Phase (Asynchronous): Only after the promise is settled will the code inside the .then() or .catch() handlers be executed.