Asynchronous processing
Before we get into the best practices let’s review how the EdgeWorkers platform handles simultaneous processing.
JavaScript is inherently a single-threaded execution environment. The EdgeWorkers platform has multiple execution environments, also known as contexts, to serve multiple requests simultaneously.
Only one of these environments can serve a specific request at a time. You might, however, want to write code that executes in an asynchronous manner. When making an HTTPS request from EdgeWorkers, you should avoid tying up the thread while waiting for a response. Instead, the code should continue to execute, either for the current request or a different request, while waiting for the HTTPS response.
To accomplish this, EdgeWorkers JavaScript code passes the job of making an HTTPS request to the underlying platform. EdgeWorkers does this by providing a callback function for the platform to invoke when the HTTPS response is available. You can also use callbacks to write more efficient code with techniques such as making multiple HTTPS requests in parallel.
About promises
In JavaScript, you can use promises to manage the execution of asynchronous code. Instead of waiting for a long-running process to return the value, JavaScript code can immediately return a promise object while the task is completing asynchronously. The promise object represents the current state of the task, “promising” to supply the final value at some point in the future. Once the asynchronous task completes successfully, the promise’s then() callback is invoked.
Within EdgeWorkers, you can use promises when making any outbound HTTP sub-request. Promises make HTTP sub-requests asynchronously, allowing code execution to continue. The httpRequest() function returns a promise that resolves when the HTTP response headers are available.
For example, the following EdgeWorkers code calls the httpRequest() function on line six. The httpRequest() function immediately returns a promise, and the execution of onClientRequest continues, adding a then() handler. The httpRequestCompleted() function is only invoked when the HTTP response is available.
Since the httpRequestCompleted function will not be called until the promise, created by httpRequest is resolved, the “continuing processing” log line will be generated first, followed later by the “response completed…” log statement. However, the onClientRequest function would already have completed prior to “response completed…” being logged. Thus you would not see this log statement in your debug output.
How can we force EdgeWorkers to wait for the log line to generate before completing? We can return our own promise from the onClientRequest function. The EdgeWorkers platform can consume a promise from an event handler. The platform will wait for the promise to resolve before considering the event handler to be complete. A wall timeout error will occur if the promise is not resolved prior to the 4 second EdgeWorkers wall time limit.
Luckily, you don’t need to create our own promise in this case. The object returned from .then() is another promise that resolves after the callback method has completed processing. Therefore, you can return the promise created by the .then() function as shown in the code sample below.
How HTTP request handlers use promises in a response
An important detail of asynchronously handling HTTP requests is that the promise returned by httpRequest() is resolved once the response headers are available. However, the body of the response may not be available yet. In the previous examples, we do not wait for the response body before continuing processing.
If the body is not yet available you need to create another promise. The .text() and .json() methods on the response object return a promise that resolves when the body is available. Both .text() and .json() are similar, with the key difference being that .text() will resolve to a string containing the response body, where .json() will resolve to an object generated from parsing the body content from JSON. (There’s also the ability to retrieve the response body as a stream.)
The code example below shows how we can wait to receive the response body before continuing.
The arrow syntax used in (response) => response.text() is shorthand to create an inline function. In this case, the function is taking one parameter, named response, and returning the result from response.text().
Best practices
Use these best practices to maximize the benefits of asynchronous processing in your custom JavaScript code.
The code samples in this section use EdgeWorkers to show how you can use promises in JavaScript. The fundamentals of promises are not specific to EdgeWorkers. If you’re interested in learning more there are many guides available including Javascript.info and MDN.
Perform multiple actions in sequence
You can use promise “chaining” to build a series of operations with each waiting until the previous one completes to start. The example below makes three HTTP requests. The previous request must complete before starting the next request. Because each then() callback handler function returns a promise, the next handler in the chain will not execute until the previous promise is resolved. Once the third request is complete, the end user receives a client response and responseProvider returns the promise.
Use Async/Await
To provide a more natural syntax when working with promises, JavaScript added the “async” and “await” keywords.
Let’s look at these one at a time starting with “async”. The first change you’ll see when using “async” is that async ensures that a function always returns a promise. Instead of having to create a Promise, such as through using Promise.resolve(), we can simply return the value and the promise will be implicitly created.
In effect, the following two code examples are equivalent. The first example explicitly creates a promise and resolves it with the object returned by createResponse(). In the next example, the async keyword results in JavaScript implicitly creating the Promise around our returned object.
The second change of note when using “async” is that it enables the use of the “await” keyword. The “await” keyword instructs JavaScript to wait on the result of a promise before continuing processing in the current function. Internally to the JavaScript engine, it is equivalent to placing the remainder of the JavaScript method into a then() handler. The examples below show the power of using async/await. The top example using async/wait is a much more natural syntax than the second example with manual promise chaining.
Handle rejected promises
Promises support error handling through the concept of a “rejected” promise.
A promise can be in one of three states:
- Pending. Until the final value is known, the promise is pending.
- Fulfilled. Once the operation completes successfully, the promise is fulfilled with the final value.
- Rejected. If an error occurs during processing, the promise is rejected with the error.
There are a couple different ways you can handle rejected promises.
The first involves the then(), catch(), and finally() functions. The then() function takes a second callback function as a parameter. It executes when the promise is rejected, as shown in the next example. If the the promise returned by httpRequest() is rejected (for example, if the request times out), the httpRequestError() method is called.
Alternatively, you can use the .catch() method to define the callback for a rejected promise.
And for code that should run regardless of success or failure, you can use the finally() method.
If you are using the async/await syntax, you can use a standard try/catch/finally block to handle errors that occur while waiting for result of a promise. The example below is equivalent to the previous example, but uses the async/await syntax to enable the more natural try/catch/finally syntax.