Overview
The async/await keywords give developers a more concise way to write asynchronous, non-blocking programs. In this chapter, we'll learn all about this syntactic sugar, a term for more concise and expressive syntax, for promises and how it drives modern software development. We will look at common uses of async/await and discuss the landscape of asynchronous programming in TypeScript. By the end of this chapter, you will be able to implement async/await keywords in TypeScript and use them to write asynchronous programs.
The previous chapter got you started on promises in TypeScript. While promises improved our ability to write asynchronous code without the ugliness of nested callbacks, developers still wanted a better way to write asynchronous code. The promise syntax is sometimes challenging for programmers with a background in the C family of languages, and so the "syntactic sugar" of async/await was proposed to be added to the ECMAScript specification.
In this chapter, we'll learn about the introduction of new asynchronous programming paradigms to the ECMAScript standard, examine the syntax, and look at their use in TypeScript. We'll also cover the new (as of the time of writing) top-level await feature, which allows asynchronous programming outside of an async function. We will again look at error handling in asynchronous programming and examine the pros and cons of using async/await syntax versus promises.
Readers who have been through the prior chapter will see that there is still some nesting involved in promises. While the flow is much easier to manage through multiple promises than it is with nested callbacks, we still have no mechanism by which we can return control to the top level.
For example, consider a getData function that returns a promise. The code that invokes this function will look something like this:
getData().then(data => {
// do something with the data
});
We don't have any means to propagate the data value to the outer scope. We couldn't deal with that value in a subsequent scope. Some programmers may attempt to write code that looks like this:
let myData;
getData().then(data => {
myData = data
});
console.log(myData);
This code will always log out undefined. It seems like it should work, but it won't because the promise callback won't be invoked until the promise returns. Asynchronous programming like this can be confusing and lead to lots of bugs. async/await solve this problem by allowing us to pause the execution of code pending the resolution of a promise. We can rewrite the preceding code using async/await syntax:
const myData = await getData();
console.log(myData);
We've gone from five lines of code to two. The synchronous operation of console.log will wait for the promise to resolve. The code is much more understandable, and we can store our variable at the top scope without nesting.
Because TypeScript is transpiled to JavaScript in most cases, we need to make sure that we select the correct target environment in order for our code to run. This topic will be dealt with in greater detail later in the chapter.
Although promises moved the needle considerably when it came to asynchronous programming paradigms, there remained a desire for a lighter syntax that relied less on explicitly declaring promise objects. Adding the async/await keywords to the ECMAScript specification would allow developers to reduce boilerplate and work with promises. The concept comes from the C# programming language, which in turn borrowed the concept of asynchronous workflows from F#.
An asynchronous function allows a program to continue normal operation even though that function call has yet to return. The program does not wait for that asynchronous function call to complete until the await keyword is found. More significantly, using await will not block the event loop. Even if we have paused part of a program to await the result of an asynchronous function call, other operations can still complete. The event loop is not blocked. For more on the event loop, return to Chapter 12, Guide to Promises in TypeScript.
The great thing about these keywords is that they are immediately compatible with promises. We can await any promise, thereby avoiding having to use the then() API. This capability means that along with the concept of promisification (see Chapter 12, Guide to Promises in TypeScript), we can use the latest syntax even when integrating with older libraries or modules. To demonstrate this, let's return to an example from the preceding chapter:
import { promises } from "fs";
promises.readFile('text.txt').then(file => console.log(file.toString()));
This example uses the promises API from the fs (filesystem) module from Node.js. The code reads a file from the local filesystem and logs the contents to the console. We can use await syntax with this code:
import { promises } from "fs";
const text = (await promises.readFile('text.txt')).toString();
console.log(text);
Note that in order to run this code, you must be able to use top-level await, which, at the time of this writing, requires a bit of extra setup. Refer to the section later in this chapter. The takeaway from this example is that we are still able to use the promises API from the fs module, even if we prefer async/await.
The maintainers of TypeScript begin work on supporting ECMAScript features when they are in stages 1 and 2 of the review process, but only formally release them when they reach stage 3.
TypeScript began offering experimental support for async functions in version 1.6, released in September 2015, and offered full support in version 1.7, released in November 2015. TypeScript programmers could work with this syntax a full year ahead of official browser and Node.js support.
Use of the async/await keywords in TypeScript does not vary much from JavaScript, but we do have an advantage in the ability to be more explicit about which functions should return promises and which should return a resolved value or throw an error.
One thing to be cognizant of when writing modern syntax in TypeScript is that most TypeScript code is transpiled to JavaScript for execution in a runtime, such as a web browser or Node.js. We need to understand the difference between transpilation and a polyfill. Transpilation will convert code from one syntax into another. In this case, we can write async/await code and transpile to an environment that only supports promise syntax. A polyfill adds missing language features. If our target environment doesn't even support promises, then transpiling async/await into promises won't do the trick. We will require a polyfill as well.
In this exercise, we will use a contrived "Hello World!" example to demonstrate how TypeScript handles the transpilation of the async /await keywords:
Note
The code files for this exercise can be found here: https://packt.link/NS8gY.
npm install
World!
Hello
World! printed before Hello.
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
}
npx tsc
No output means that it executed successfully.
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
//тАж.
sayHello();
console.log('World!');
Note
The complete code can be found here: https://packt.link/HSmyX.
We can execute the transpiled code by typing node target.js at the command prompt and we'll see that we get the same output as before.
Promises are not part of the ECMAScript5 specification, so to generate code that will work in an ECMAScript5 environment, the transpiler had to create __awaiter and __generator functions to support promise-like functionality.
{
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
}
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
const sayHello = () => __awaiter(void 0, void 0, void 0, function* () {
yield new Promise((resolve) => setTimeout(() => resolve(console.log('Hello')), 1));
});
sayHello();
console.log('World!');
The transpiled code is now 15 lines instead of over 50 because ECMAScript6 is much closer to supporting all the functionality we need than es5 is. The async/await keywords are not supported in ECMAScript6, but promises are, so TypeScript is leveraging promises to make the outputted code more concise.
"use strict";
const sayHello = async () => {
await new Promise((resolve) => setTimeout(() => resolve(console.log('Hello')), 1));
};
sayHello();
console.log('World!');
That's very similar to our source code! Since async/await are supported in the latest ECMAScript specification, there's no need to transform.
npx tsc
target.ts:1:18 - error TS2705: An async function or method in ES5/ES3 requires the 'Promise' constructor. Make sure you have a declaration for the 'Promise' constructor or include 'ES2015' in your `--lib` option.
1 const sayHello = async () => {
~~~~~~~~~~~~~
target.ts:2:13 - error TS2693: 'Promise' only refers to a type, but is being used as a value here.
2 await new Promise((resolve) =>
~~~~~~~
target.ts:2:22 - error TS7006: Parameter 'resolve' implicitly has an 'any' type.
2 await new Promise((resolve) =>
It doesn't work.
% npx tsc
target.ts:3:30 - error TS2345: Argument of type 'void' is not assignable to parameter of type '{} | PromiseLike<{}> | undefined'.
3 setTimeout(() => resolve(console.log('Hello')))
This aspect of TypeScript can be confusing for newcomers. TypeScript will not provide a polyfill for missing promises, but it will provide transformations to syntax that is functionally equivalent.
So how do we choose a compilation target? It's generally safe to use ES2017 or above unless you need to support outdated browsers, such as Internet Explorer, or deprecated Node.js versions. Sometimes, we have no choice but to support outdated browsers due to customer needs, but if we have any control over a Node.js runtime environment, it's advisable to update to a current, supported version. Doing this should allow us to use the latest TypeScript features.
The two new keywords, async/await, are often found together, but not always. Let's look at the syntax for each of them individually.
The async keyword modifies a function. If a function declaration or function expression is used, it is placed before the function keyword. If an arrow function is used, the async keyword is placed before the argument list. Adding the async keyword to a function will cause the function to return a promise.
For example:
function addAsync(num1: number, num2: number) {
return num1 + num2;
}
Just adding the async keyword to this simple function will make this function return a promise, which is now awaitable and thenable. Since there's nothing asynchronous in the function, the promise will resolve immediately.
The arrow function version of this could be written as follows:
const addAsync = async (num1: number, num2: number) => num1 + num2;
This exercise illustrates how adding the async keyword to a function makes it return a promise:
Note
The code files for this exercise can be found here: https://packt.link/BgujE.
export const fn = async () => {
return 'A Promise';
};
const result = fn();
console.log(result);
You might expect this program to log out A Promise, but let's see what actually happens when we run it:
npx ts-node async.ts
Promise { 'A Promise' }
npx ts-node async.ts
A Promise
export const fn = () => {
return Promise.resolve('A Promise');
};
const result = fn();
console.log(result);
npx ts-node async.ts
Promise { 'A Promise' }
Since we're using TypeScript and return types can be inferred, modifying a function with async guarantees that TypeScript will always see the function as returning a promise.
The async keyword causes the function it modifies to be wrapped in a promise. Whether you choose to do that explicitly by declaring a promise or by using the async keyword is often a matter of taste and style.
How can we resolve an async function? We'll come to await in a moment, but what about using then and the promise chaining we learned about in Chapter 12, Guide to Promises in TypeScript. Yes, that is also possible.
This exercise will teach you how to resolve an async function using then:
Note
The code files for this exercise can be found here: https://packt.link/4Bo4c.
export const fn = async () => {
return 'A Promise';
};
const result = fn();
result.then((message) => console.log(message));
A Promise
Even though we never explicitly declared a promise object, the use of async has ensured that our function will always return a promise.
The second half of this combo perhaps has greater value. The await keyword will attempt to resolve any promise before continuing. This will get us out of then chaining and allow us to write code that appears to be synchronous. One great benefit of using await is if we want to assign the result of an asynchronous call to some value and then do something with the value. Let's look at how that's done in a promise:
asyncFunc().then(result => {
// do something with the result
});
That can work fine and, in fact, this kind of syntax is used widely, but it breaks down a little if we need to do something tricky with chaining:
asyncFuncOne().then(resultOne => {
asyncFuncTwo(resultOne).then(resultTwo => {
asyncFuncThree(resultTwo).then(resultThree => {
// do something with resultThree
});
});
});
But wait a minute. I thought promises were supposed to get rid of callback hell?! It's actually not that ideal for this kind of chaining. Let's try using await instead:
const resultOne = await asyncFuncOne();
const resultTwo = await asyncFuncTwo(resultOne);
const resultThree = await asyncFuncThree(resultTwo);
// do something with resultThree
Most programmers would agree that this syntax is much cleaner and, in fact, this is one of the primary reasons why async/await were added to the language.
This exercise will show you how to resolve a promise using await:
Note
The code files for this exercise can be found here: https://packt.link/mUzGI.
export const fn = async () => {
return 'A Promise';
};
const resolveIt = async () => {
const result = await fn();
console.log(result);
};
resolveIt();
Here we declare two async functions. One of them calls the other using await to resolve the promise and it should print out the string, rather than an unresolved promise.
A Promise
Why did we need to wrap await in a second function? That is because normally, await cannot be used outside of an async function. We'll discuss the top-level await feature later in this chapter, which is an exception to this rule. What about mixing await with promises? This can certainly be done.
This exercise teaches you how you can use await with promises:
Note
The code files for this exercise can be found here: https://packt.link/mMDiw.
export const resolveIt = async () => {
const result = await Promise.resolve('A Promise');
console.log(result);
};
resolveIt();
A Promise
export const resolveIt = async () => {
const p = new Promise((resolve) => resolve('A Promise'));
const result = await p;
console.log(result);
};
resolveIt();
This code functions exactly the same:
A Promise
The preceding exercises on async functions and promises are simply two different ways of expressing the exact same operation in TypeScript. Likewise, using await and resolving a promise with then are equivalent. The async/await keywords are what's known as "syntactic sugar," or code structures that enable more expressive syntax without changing the behavior of the program.
This means it is possible and, at times, even advisable to mix async/await syntax with promises. A very common reason for doing this would be because you are working with a library that was written to use promises, but you prefer async/await syntax. Another reason for mixing the two would be to handle exceptions more explicitly. We'll deal with exception handling in detail later in this chapter.
We've been over how to turn then chaining into await, but what about catch? If a promise is rejected, the error will bubble up and must be caught in some way. Failing to catch an exception in the async/await world is just as damaging as failing to catch a promise rejection. In fact, it's exactly the same and async/await is just syntactic sugar on top of promises.
Failing to handle a rejected promise can lead to system failure where a program running in a web browser crashes, resulting in blank pages or broken functionality, thereby driving users away from your site. A failure to handle a rejected promise on the server side may cause a Node.js process to exit and a server to crash. Even if you have a self-healing system that attempts to bring your server back online, whatever job you were attempting to complete will have failed and frequently repeated restarts will make your infrastructure more expensive to run.
The most straightforward way to handle these errors is with try and catch blocks. This syntax is not unique to async/await and has been part of the ECMAScript specification since ECMAScript3. It is very simple and straightforward to use:
try {
await someAsync();
} catch (e) {
console.error(e);
}
Just as you can catch an error thrown from any of several chained promises, you can implement a similar pattern here:
try {
await someAsync();
await anotherAsync();
await oneMoreAsync();
} catch (e) {
console.error(e);
}
There may be cases where finer-grained exception handling is required. It is possible to nest these structures:
try {
await someAsync();
try {
await anotherAsync();
} catch (e) {
// specific handling of this error
}
await oneMoreAsync();
} catch (e) {
console.error(e);
}
However, writing code such as this negates most of the benefits of the async/await syntax. A better solution would be to throw specific error messages and test for them:
try {
await someAsync();
await anotherAsync();
await oneMoreAsync();
} catch (e) {
if(e instanceOf MyCustomError) {
// some custom handling
} else {
console.error(e);
}
}
With this technique, we can handle everything in the same block and avoid nesting and messy-looking code structures.
Let's see how we can implement error handling in a simple example. In this exercise, we will intentionally and explicitly throw an error from an async function and see how that implements the operation of our program:
Note
The code files for this exercise can be found here: https://packt.link/wbA8E.
export const errorFn = async () => {
throw new Error('An error has occurred!');
};
const asyncFn = async () => {
await errorFn();
};
asyncFn();
(node:29053) UnhandledPromiseRejectionWarning: Error: An error has occurred!
at Object.exports.errorFn (/workshop/async-chapter/src/error.ts:2:9)
at asyncFn (/workshop/async-chapter/src/error.ts:6:9)
at Object.<anonymous> (/workshop/async-chapter/src/error.ts:9:1)
at Module._compile (internal/modules/cjs/loader.js:1138:30)
at Module.m._compile (/workshop/async-chapter/node_modules/ts-node/src/index.ts:858:23)
at Module._extensions..js (internal/modules/cjs/loader.js:1158:10)
at Object.require.extensions.<computed> [as .ts] (/workshop/async-chapter/node_modules/ts-node/src/index.ts:861:12)
at Module.load (internal/modules/cjs/loader.js:986:32)
at Function.Module._load (internal/modules/cjs/loader.js:879:14)
at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:71:12)
(node:29053) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag `--unhandled-rejections=strict` (see https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). (rejection id: 2)
(node:29053) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
Notice the deprecation warning. Not only is this an ugly stack trace, at some point in the future, exceptions such as this one will cause the Node.js process to exit. We clearly need to handle this exception!
export const errorFn = async () => {
throw new Error('An error has occurred!');
};
const asyncFn = async () => {
try {
await errorFn();
} catch (e) {
console.error(e);
}
};
asyncFn();
Error: An error has occurred!
at Object.exports.errorFn (/workshop/async-chapter/src/error.ts:2:9)
at asyncFn (/workshop/async-chapter/src/error.ts:7:11)
at Object.<anonymous> (/workshop/async-chapter/src/error.ts:13:1)
at Module._compile (internal/modules/cjs/loader.js:1138:30)
at Module.m._compile (/workshop/node_modules/ts-node/src/index.ts:858:23)
at Module._extensions..js (internal/modules/cjs/loader.js:1158:10)
at Object.require.extensions.<computed> [as .ts] (/workshop/node_modules/ts-node/src/index.ts:861:12)
at Module.load (internal/modules/cjs/loader.js:986:32)
at Function.Module._load (internal/modules/cjs/loader.js:879:14)
at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:71:12)
Of course, that message only appears because we explicitly logged it out. We could instead choose to throw a default value or perform some other operation instead of logging the error.
const primaryFn = async () => {
throw new Error('Primary System Offline!');
};
const secondaryFn = async () => {
console.log('Aye aye!');
};
const asyncFn = async () => {
try {
await primaryFn();
} catch (e) {
console.warn(e);
secondaryFn();
}
};
asyncFn();
In this case, we just throw a warning and fall back to the secondary system because this program was designed to be fault-tolerant. It's still a good idea to log the warning so that we can trace how our system is behaving. One more variation of this for now.
export const errorFN = async () => {
throw new Error('An error has occurred!');
};
const asyncFn = async () => {
await errorFN();
};
try {
asyncFn();
} catch (e) {
console.error(e);
}
Error: Primary System Offline!
at primaryFn (C:UsersMaheshDocumentsChapter13_TypeScriptExercise13.06error-secondary.ts:2:9)
at asyncFn (C:UsersMaheshDocumentsChapter13_TypeScriptExercise13.06error-secondary.ts:11:11)
at Object.<anonymous> (C:UsersMaheshDocumentsChapter13_TypeScriptExercise13.06error-secondary.ts:18:1)
at Module._compile (internal/modules/cjs/loader.js:1063:30)
at Module.m._compile (C:UsersMaheshAppDataRoaming pm-cache\_npx13000 ode_modules s-nodesrcindex.ts:1056:23)
at Module._extensions..js (internal/modules/cjs/loader.js:1092:10)
at Object.require.extensions.<computed> [as .ts] (C:UsersMaheshAppDataRoaming pm-cache\_npx13000 ode_modules s-nodesrcindex.ts:1059:12)
at Module.load (internal/modules/cjs/loader.js:928:32)
at Function.Module._load (internal/modules/cjs/loader.js:769:14)
at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:72:12)
Aye aye!
You may assume that the program might work the same as putting try and catch inside asyncFn, but actually, it will behave the same as no error handling at all. That's because we aren't awaiting the function inside the try block.
Top-level await is a feature that allows the use of the await keyword at the module level, outside of any function. This allows a number of interesting patterns, such as waiting for a dependency to fully load by calling an asynchronous function before attempting to use it. Someday, top-level await may support some very exciting functional programming paradigms, but at the time of writing, it is still technically in preview mode, and so is not ready for widespread use. You may be reading this book at a time when top-level await is widely available and supported, and if so, you should definitely give it a look!
Writing code with top-level await is very straightforward. Here is a very short program that attempts to make use of it:
export const fn = async () => {
return 'awaited!';
};
console.log(await fn());
This looks fine. Now let's see what happens when we try to execute it:
тип Unable to compile TypeScript:
src/top-level-await.ts:5:13 - error TS1378: Top-level 'await' expressions are only allowed when the 'module' option is set to 'esnext' or 'system', and the 'target' option is set to 'es2017' or higher.
5 console.log(await fn());
~~~~~
It's not supported, but it gives me some pointers. How can we make this work?
Top-level await requires NodeJS 14.8 or greater. This version of NodeJS entered LTS (long-term service) in October of 2020 and so is still new at the time of this writing. You can check your NodeJS version on the command line with node -v. If you aren't running version 14.8 or greater, there are some good utilities like nvm and n that will allow you to switch your version easily.
That, however, doesn't fix the problem. It seems that I will need to change my tsconfig.json target property to es2017 or higher and set the module property to esnext. Adding the module property means that I want to use ES modules, which is a relatively new way to handle modules and is beyond the scope of this book. To enable ES modules, I need to set the type property in my package.json file to module.
Now I've updated a couple of JSON files and am ready to try again:
TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".ts" for /workshop/async-chapter/src/top-level-await.ts
at Loader.defaultGetFormat [as _getFormat] (internal/modules/esm/get_format.js:65:15)
at Loader.getFormat (internal/modules/esm/loader.js:113:42)
at Loader.getModuleJob (internal/modules/esm/loader.js:243:31)
at Loader.import (internal/modules/esm/loader.js:177:17)
It still isn't working. I'll need to do one more thing to make this work, and that is to enable the experimental feature in Node.js and instruct TS Node to allow ES modules (esm). This requires a longer command:
node --loader ts-node/esm.mjs top.ts
(node:91445) ExperimentalWarning: --experimental-loader is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
awaited!
But it works. Top-level await will likely become much easier and more intuitive to work with in the months and years ahead, so make sure to check the latest documentation for your runtime.
In addition to the standard next and catch methods exposed by promises, there are a number of other convenience methods, such as all, allSettled, any, and race, that make working with promises nicer. How can they be used in the async/await world? They can actually work together quite nicely. For example, here is a use of Promise.all that employs then and catch. Given three promises, p1, p2, and p3:
Promise.all([p1, p2, p3])
.then(values => console.log(values))
.catch(e => console.error(e));
There isn't any kind of awaitAll operator, so if we want to execute our promises in parallel, we're still going to need to use Promise.all, but we can avoid chaining then and catch if we choose to:
try {
const values = await Promise.all([p1, p2, p3]);
console.log(values);
} catch (e) {
console.error(e);
}
In this case, we might feel like the code isn't improved by the addition of await, since we've actually expanded it from three lines to six. Some may find this form more readable. As always, it's a matter of personal or team preference.
In this exercise, we will build a small web application using the popular Express framework. Although Express was written for the JavaScript language, typings have been published for it and it is fully usable with TypeScript. Express is an unopinionated, minimalist framework for building web applications. It's one of the oldest and most popular frameworks in use today.
For our simple application, we'll start a web server on port 8888 and accept GET requests. If that request has a name parameter in the query string, we will log the name in a file called names.txt. Then we'll greet the user. If there's no name in the query string, we log nothing and print out Hello World!:
Note
The code files for this exercise can be found here: https://packt.link/cG4r8.
Let's get started by installing the Express framework and typings.
Remember the -D flag means that it's a devDependency that can be managed differently from a production dependency, although its use is optional.
import express, { Request, Response } from 'express';
const app = express();
app.get('/', (req: Request, res: Response) => {
res.send('OK');
});
app.listen(8888);
This looks very much like your standard starter Express app, other than we're giving types to the Request and Response objects. This is already enormously useful as we'll be able to use IntelliSense and ascertain what methods we can call on those objects without having to look them up.
Our requirements say that we need to listen for a name parameter in the query string. We might see a request that looks like http://localhost:8888/?name=Matt, to which we should respond Hello Matt!.
The query string is in the Request object. If we delve into the typings, it is typed as follows:
interface ParsedQs { [key: string]: undefined | string | string[] | ParsedQs | ParsedQs[] }
This basically means that it is a hash of key/value pairs and nested key/value pairs. In our case, we would expect to see a query object that looks like { name: 'Matt' }. Thus, we can get the name attribute by using const { name } = req.query;. Then we can respond to the request with something like res.send(`Hello ${name ?? 'World'}!`);. In this case, we're using the nullish coalesce operator (??) to say that we'll fall back to the World string if the name variable has a nullish (null or undefined) value. We could also use the fallback or logical OR operator, ||.
import express, { Request, Response } from 'express';
const app = express();
app.get('/', (req: Request, res: Response) => {
const { name } = req.query;
res.send(`Hello ${name ?? 'World'}!`);
});
app.listen(8888);
import { promises } from 'fs';
import { resolve } from 'path';
if (name) {
await promises.appendFile(resolve(__dirname, 'names.txt'), `${name} `);
}
That's almost it, but we should have a warning by now that our handler function isn't properly async. All we need to do is add the async keyword to it.
import express, { Request, Response } from 'express';
import { promises } from 'fs';
import { resolve } from 'path';
const app = express();
app.get('/', async (req: Request, res: Response) => {
const { name } = req.query;
if (name) {
await promises.appendFile(resolve(__dirname, 'names.txt'), `${name} `);
}
res.send(`Hello ${name ?? 'World'}!`);
});
app.listen(8888);
The names.txt file will increment as follows:
In contrast to Express, NestJS is a highly opinionated and fully featured framework for building TypeScript applications. NestJS can be used to quickly bootstrap an application. It provides out-of-the-box support for middleware, GraphQL, and Websockets. It ships with ESLint, a dependency injection framework, a test framework, and many other useful things. Some developers really enjoy working with such a full-featured framework and others find all the boilerplate oppressive and prefer to work with something more bare-bones, such as Express:
Note
The code files for this exercise can be found here: https://packt.link/blRq3.
Let's bootstrap a new NestJS application and give it a closer look.
npm i -g @nestjs/cli
nest new async-nest
Here the project is named async-nest. You can name it differently. NestJS will automatically install all dependencies and bootstrap a bare-bones application.
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
}
bootstrap();
NestJS is built on top of Express. This code will create a new Express application. The internals of Express are not exposed to you as you write NestJS code, but you always have the option to drop down to them if you need something not supported by NestJS.
Let's go over a few useful commands that you can start using immediately. If you type npm test (or npm t), you'll launch a test run by the Jest framework. This test launches an instance of your application, invokes it, and then shuts it down after verifying the expected response was received. NestJS ships with fixtures that allow a light version of your app to be tested.
It's a great idea to continue adding unit and integration tests to your app as you work on it. TypeScript can help you ensure code correctness, but only tests will guarantee that your app is behaving as it should.
Another useful command is npm run lint. This will check your code style and notify you of any issues with it by using the popular ESLint library.
Now that we've seen this simple Hello World app done in two very different frameworks, let's add the same greeting and logging functionality that we did in Exercise 13.07: async/await in Express.js.
export class AppService {
getHello(name: string): string {
return `Hello ${name}!`;
}
}
This is a simple refactor. If we check app.controller.ts, we'll see that our IDE is now telling us that getHello needs an argument and we're not done yet.
In the Express application, we found our query parameter on the built-in Request object. You could do the same thing in NestJS, but it's more common and preferable to use a decorator. Decorators are special functions that wrap other functions. They are sometimes called higher-order functions and are similar to aspects of languages such as Java.
The decorator we want to use is @Query, which takes an argument of the name of the query parameter and then binds that parameter to one of our function arguments.
import { Controller, Get, Query } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(@Query('name') name: string = 'World'): string {
return this.appService.getHello(name);
}
}
In a full-scale application, we'd probably want to build a separate logging service class. For our purposes, we can implement that as a separate async method. Add the method to app.service.ts and call it with await from getHello. Test it to be sure that it's working correctly.
There are a few gotchas here. One is that NestJS is automatically transpiling and serving your code from a folder called dist, so you'll find your names.txt file in there once you start logging names. But the bigger trick here is that in order to await the logging, we need to make getHello in app.service.ts into an async method. This, in turn, will mean that getHello in app.controller.ts must also be async. What will changing these methods to async do to our app? Nothing! NestJS already knows how to resolve the promises before returning the request.
describe('root', () => {
it('should return "Hello World!"', async () => {
expect(await appController.getHello()).toBe('Hello World!');
});
});
The test should now pass. Try adding another test with an argument.
TypeORM is an object relational mapper written in, and for, TypeScript. TypeORM supports many popular databases, such as MySQL, Postgres, SQL Server, SQLite, and even MongoDB and Oracle. TypeORM is often used in NestJS applications, so in this exercise we will add a local in-memory SQLite database to work with our NestJS application.
In this exercise, you will build another REST service to help us keep track of the promises we make. Since Promise is the name of a built-in object in TypeScript, let's use the term "pledge" so we can differentiate domain concepts from language abstractions:
Note
The code files for this exercise can be found here: https://packt.link/ZywYh.
nest new typeorm-nest
nest g module pledge
This command will generate a new module under the /pledge subdirectory.
nest g controller pledge
nest g service pledge
npm i @nestjs/typeorm sqlite3 typeorm
TypeORM maps database tables to TypeScript entities by means of decorators on plain objects.
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
@Entity()
export class Pledge {
@PrimaryGeneratedColumn()
id: number;
@Column()
desc: string;
@Column()
kept: boolean;
}
For this entity, we're using a few specialized decorators, such as PrimaryGeneratedColumn. These decorators can be very powerful but often rely on underlying database functionality. Because SQLite can generate an ID for our table, TypeORM is able to expose that in a declarative way with a decorator, but if it couldn't, this wouldn't work. It's always good to check the documentation before proceeding with a new implementation.
Now that we have an entity, we need to provide configuration to TypeORM about what our database is and where to find it, as well as what entities we want to map. For databases such as MySQL and Postgres, this might include a URI as well as database credentials. Since SQLite is a file-based database, we will just provide the name of the file we want to write.
Note that production database credentials should always be handled safely, and the best practices for doing so are beyond the scope of this book, but suffice to say that they shouldn't be checked into your version control.
TypeOrmModule.forRoot({
type: 'sqlite',
database: 'db',
entities: [Pledge],
synchronize: true,
}),
import { TypeOrmModule } from '@nestjs/typeorm';
import { Pledge } from './pledge/pledge.entity';
We're letting NestJS know that our application will use a SQLite database and will manage the Pledge entity. By setting synchronize: true, we are telling TypeORM to automatically create any entities that don't already exist in the database when the application starts. This setting should NOT be used in production as it may cause data loss. TypeORM supports migrations for managing databases in production environments, another topic beyond the scope of this book.
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { PledgeController } from './pledge.controller';
import { Pledge } from './pledge.entity';
import { PledgeService } from './pledge.service';
@Module({
controllers: [PledgeController],
imports: [TypeOrmModule.forFeature([Pledge])],
providers: [PledgeService],
})
export class PledgeModule {}
This will allow the Pledge entity to be used by pledge.service.ts. Again, NestJS has quite a lot of boilerplate, which may be jarring to developers who are used to unopinionated ExpressJS workflows. This module system can help us to isolate our application into functional areas. It's a good idea to understand the benefits of a structured application before deciding whether a framework such as NestJS is right for your application or team.
We can now start to build out our Pledge service. TypeORM supports both Active Record, where an entity itself has methods for reading and updating, and Data Mapper, where such functionality is delegated to a Repository object. We will follow the Data Mapper pattern in this exercise.
import { Injectable } from '@nestjs/common';
import { Pledge } from './pledge.entity';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
@Injectable()
export class PledgeService {
constructor(
@InjectRepository(Pledge)
private pledgeRepository: Repository<Pledge>,
) {}
findAll(): Promise<Pledge[]> {
return this.pledgeRepository.find();
}
}
We've now exposed a findAll method, which will query the database for all the Pledge entities and return them in an array.
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { DeleteResult, Repository } from 'typeorm';
import { Pledge } from './pledge.entity';
@Injectable()
export class PledgeService {
constructor(
@InjectRepository(Pledge)
private pledgeRepository: Repository<Pledge>,
) {}
delete(id: number): Promise<DeleteResult> {
return this.pledgeRepository.delete(id);
}
findAll(): Promise<Pledge[]> {
return this.pledgeRepository.find();
}
findOne(id: number): Promise<Pledge> {
return this.pledgeRepository.findOne(id);
}
save(pledge: Pledge): Promise<Pledge> {
return this.pledgeRepository.save(pledge);
}
}
We can get pretty far using just repository methods, which will generate SQL queries for us, but it's also possible to use SQL or a query builder with TypeORM.
import { Body, Controller, Delete, Get, Param, Post } from '@nestjs/common';
import { DeleteResult } from 'typeorm';
import { Pledge } from './pledge.entity';
import { PledgeService } from './pledge.service';
@Controller('pledge')
export class PledgeController {
constructor(private readonly pledgeService: PledgeService) {}
@Delete(':id')
deletePledge(@Param('id') id: number): Promise<DeleteResult> {
return this.pledgeService.delete(id);
}
@Get()
getPledges(): Promise<Pledge[]> {
return this.pledgeService.findAll();
}
@Get(':id')
getPledge(@Param('id') id: number): Promise<Pledge> {
return this.pledgeService.findOne(id);
}
@Post()
savePledge(@Body() pledge: Pledge): Promise<Pledge> {
return this.pledgeService.save(pledge);
}
}
This controller will automatically inject the service and can then easily map service methods to API endpoints using decorators and dependency injection.
In this exercise, we used NestJS and TypeORM to build a real web API that can create and retrieve records from a SQLite database. Doing this isn't very different from using a real production-grade database such as MySQL or PostgreSQL.
In this activity, we will refactor a function that chains promises together to use await. You are supplied with a starter program that is meant to simulate the creation of DOM elements for a website and render them one after another. In reality, most sites will want to render in parallel, but it's possible that information from one component might inform the rendering of another. It is good enough for example purposes in any case:
Note
The code files for this activity can be found here: https://packt.link/L5r76.
The code for the starter program (refactor.ts) is as follows:
export class El {
constructor(private name: string) {}
render = () => {
return new Promise((resolve) =>
setTimeout(
() => resolve(`${this.name} is resolved`),
Math.random() * 1000
)
);
};
}
const e1 = new El('header');
const e2 = new El('body');
const e3 = new El('footer');
const renderAll = () => {
e1.render().then((msg1) => {
console.log(msg1);
e2.render().then((msg2) => {
console.log(msg2);
e3.render().then((msg3) => {
console.log(msg3);
});
});
});
};
renderAll();
Once you run the program, you should get the following output:
header is resolved
body is resolved
footer is resolved
Note
The solution to this activity can be found via this link.
Asynchronous programming has come a long way in the past 10 years and the introduction of async/await has continued to move it forward. Although not perfect for every use case, this syntactic sugar has proven very popular with the TypeScript community and has gained widespread acceptance in popular libraries and frameworks.
In this chapter, we went over async/await syntax, how it came to be part of the language, and how the use of this syntax is actually complimentary to promises. We then toured several popular frameworks in use by TypeScript developers to see how application developers use promises and asynchronous programming to develop powerful web applications.
This concludes this book's study of language features. The next chapter will look at React for building user interfaces using TypeScript.
18.218.38.125