How to implement a promise-based API
In the last article we discussed how to use APIs that return promises. In this article we'll look at the other side — how to implement APIs that return promises. This is a much less common task than using promise-based APIs, but it's still worth knowing about.
Prerequisites: | A reasonable understanding of JavaScript fundamentals, including event handling and the basics of promises. |
---|---|
Objective: | To understand how to implement promise-based APIs. |
Generally, when you implement a promise-based API, you'll be wrapping an asynchronous operation, which might use events, or plain callbacks, or a message-passing model. You'll arrange for a Promise
object to handle the success or failure of that operation properly.
Implementing an alarm() API
In this example we'll implement a promise-based alarm API, called alarm()
. It will take as arguments the name of the person to wake up and a delay in milliseconds to wait before waking the person up. After the delay, the function will send a "Wake up!" message, including the name of the person we need to wake up.
Wrapping setTimeout()
We'll use the
setTimeout()
API to implement our alarm()
function. The setTimeout()
API takes as arguments a callback function and a delay, given in milliseconds. When setTimeout()
is called, it starts a timer set to the given delay, and when the time expires, it calls the given function.
In the example below, we call setTimeout()
with a callback function and a delay of 1000 milliseconds:
<
button
id
=
"
set-alarm"
>
Set alarm
</
button
>
<
div
id
=
"
output"
>
</
div
>
const
output =
document.
querySelector
(
"#output"
)
;
const
button =
document.
querySelector
(
"#set-alarm"
)
;
function
setAlarm
(
)
{
setTimeout
(
(
)
=>
{
output.
textContent =
"Wake up!"
;
}
,
1000
)
;
}
button.
addEventListener
(
"click"
,
setAlarm)
;
The Promise() constructor
Our alarm()
function will return a Promise
that is fulfilled when the timer expires. It will pass a "Wake up!" message into the then()
handler, and will reject the promise if the caller supplies a negative delay value.
The key component here is the
Promise()
constructor. The Promise()
constructor takes a single function as an argument. We'll call this function the executor
. When you create a new promise you supply the implementation of the executor.
This executor function itself takes two arguments, which are both also functions, and which are conventionally called resolve
and reject
. In your executor implementation, you call the underlying asynchronous function. If the asynchronous function succeeds, you call resolve
, and if it fails, you call reject
. If the executor function throws an error, reject
is called automatically. You can pass a single parameter of any type into resolve
and reject
.
So we can implement alarm()
like this:
function
alarm
(
person,
delay
)
{
return
new
Promise
(
(
resolve,
reject
)
=>
{
if
(
delay <
0
)
{
throw
new
Error
(
"Alarm delay must not be negative"
)
;
}
setTimeout
(
(
)
=>
{
resolve
(
`
Wake up,
${
person}
!
`
)
;
}
,
delay)
;
}
)
;
}
This function creates and returns a new Promise
. Inside the executor for the promise, we:
-
check that
delay
is not negative, and throw an error if it is. -
call
setTimeout()
, passing a callback anddelay
. The callback will be called when the timer expires, and in the callback we callresolve
, passing in our"Wake up!"
message.
Using the alarm() API
This part should be quite familiar from the last article. We can call alarm()
, and on the returned promise call then()
and catch()
to set handlers for promise fulfillment and rejection.
const
name =
document.
querySelector
(
"#name"
)
;
const
delay =
document.
querySelector
(
"#delay"
)
;
const
button =
document.
querySelector
(
"#set-alarm"
)
;
const
output =
document.
querySelector
(
"#output"
)
;
function
alarm
(
person,
delay
)
{
return
new
Promise
(
(
resolve,
reject
)
=>
{
if
(
delay <
0
)
{
throw
new
Error
(
"Alarm delay must not be negative"
)
;
}
setTimeout
(
(
)
=>
{
resolve
(
`
Wake up,
${
person}
!
`
)
;
}
,
delay)
;
}
)
;
}
button.
addEventListener
(
"click"
,
(
)
=>
{
alarm
(
name.
value,
delay.
value)
.
then
(
(
message
)
=>
(
output.
textContent =
message)
)
.
catch
(
(
error
)
=>
(
output.
textContent =
`
Couldn't set alarm:
${
error}
`
)
)
;
}
)
;
Try setting different values for "Name" and "Delay". Try setting a negative value for "Delay".
Using async and await with the alarm() API
Since alarm()
returns a Promise
, we can do everything with it that we could do with any other promise: promise chaining, Promise.all()
, and async
/ await
:
const
name =
document.
querySelector
(
"#name"
)
;
const
delay =
document.
querySelector
(
"#delay"
)
;
const
button =
document.
querySelector
(
"#set-alarm"
)
;
const
output =
document.
querySelector
(
"#output"
)
;
function
alarm
(
person,
delay
)
{
return
new
Promise
(
(
resolve,
reject
)
=>
{
if
(
delay <
0
)
{
throw
new
Error
(
"Alarm delay must not be negative"
)
;
}
setTimeout
(
(
)
=>
{
resolve
(
`
Wake up,
${
person}
!
`
)
;
}
,
delay)
;
}
)
;
}
button.
addEventListener
(
"click"
,
async
(
)
=>
{
try
{
const
message =
await
alarm
(
name.
value,
delay.
value)
;
output.
textContent =
message;
}
catch
(
error)
{
output.
textContent =
`
Couldn't set alarm:
${
error}
`
;
}
}
)
;