Promises with Async/Await
Promises are one approach to asynchronous programming. A promise is an object that represents the eventual completion or failure of an asynchronous operation and its resulting value. Promises have three states: pending, resolved, or rejected.Functions which perform asynchronous operations (such as interacting with the file system or a database) can return a promise which is then resolved later. If that promise can't be kept (something is rejected) we can catch the error and deal with it.
Then and Catch
getDataFromFile()// promise is resolved.then((result) => {doSomethingWithData(result)})// promise is rejected.catch((err) => {handleError(err)})
This example is about as simple as it gets. We don't need to understand the exact details of how to return a promise in order to use getDataFromFile. We just need to expect that:
The
thenfunction will calldoSomethingWithDataif there is no error.doSomethingWithDatacan be passed a value.The
catchfunction will callhandleErrorif there is an error, andhandleErrorwill be passed an error (often a JavaScriptErrorobject).
If getDataFromFile returns a resolved promise, then will be called (and catch won't be). If it returns a rejected promise, catch will be called (and then won't be).
If you are familiar with callbacks, you'll notice it's like a callback that has had the err and the data handling parts separated. The same thing might be written like this with callbacks:
getDataFromFileCallback(function(err, data) {if (err) {console.error(err.message)} else {console.log(data)}})
Promise Chains
We can also string together quite long 'promise chains' which define the order certain tasks should occur in:
getDataFromFile().then((uncheckedMessyData) => {return checkTheData(uncheckedMessyData)}).then((messyData) => {return modifyTheData(messyData)}).then((tidyData) => {displayTheData(tidyData)}).catch((err) => {handleError(err)})
So long as each function in the chain returns a data object, this will help ensure everything takes place in the correct order. For example, displayTheData always gets called after modifyTheData.
Async/Await
The async/await syntax is a more concise way to work with promises. It was introduced in ECMAScript 2017 (ES8) to simplify the process of writing asynchronous code, making it look more like synchronous code.
Here's the original example from above using async/await:
const fetchDataAndDoSomething = async () => {try {// promise is resolvedconst data = await getDataFromFile()doSomethingWithData(data)}catch (err) {// promise is rejectedconsole.log(err)}}
In the async/await example:
- The
fetchDataAndDoSomethingfunction is marked with theasynckeyword, indicating that it returns a promise. - Inside the function, the
awaitkeyword is used to pause execution until the promise is resolved. This makes the asynchronous code look more like synchronous code. - The
try/catchblock is used for error handling, making it more readable and manageable.
You can also chain promises with async/await. Here's the chained promise code from above written with async/await:
const processDataFromFile = async () => {try {const uncheckedMessyData = await getDataFromFile();const messyData = await checkTheData(uncheckedMessyData);const tidyData = await modifyTheData(messyData);displayTheData(tidyData);} catch (err) {handleError(err);}}
In this refactored code:
- Each
.then()block is replaced with anawaitstatement, making the code more linear and easier to read. - The
try/catchblock is used to handle errors, similar to the.catch()at the end of the original code. - The
awaitkeyword is used to wait for each asynchronous operation to complete before moving on to the next step in the process.
In summary, async/await is a syntactic sugar built on top of promises, providing a cleaner and more readable way to work with asynchronous code. It simplifies the process of handling promises, especially when dealing with multiple asynchronous operations or when chaining promises.
Catch and TypeScript
In TypeScript, your linter may show a warning or error if you don’t explicitly type the catch parameter, especially for the strict TypeScript setting. Writing catch (err: unknown) is safer than just catch (err) because it forces you to treat the error as something you don’t yet trust. JavaScript allows anything to be 'thrown' as an error (like a string or even null), so marking the error as unknown helps prevent mistakes like trying to access properties that might not exist.
To handle the error safely, we check if (err instanceof Error) to see if it's a real Error object. If it is, we can safely use err.message to show a helpful error message. If it’s not, we fall back to a generic message like "Something went wrong". This approach keeps your code more robust and avoids unexpected crashes.
catch (err: unknown) {if (err instanceof Error){console.log(err.message)}else{console.log('Something went wrong')}}