Javascript Promise Create
When we use the Fetch API, the Promises are created behind the scenes, by the fetch() function. But what if the API we want to work with doesn’t support Promises?
For example, setTimeout was created before Promises existed. If we want to avoid Callback Hell when working with timeouts, we’ll need to create our own Promises.
Here’s what the syntax looks like:
const demoPromise = new Promise((resolve) => {
// Do some sort of asynchronous work, and then
// call `resolve()` to fulfill the Promise.
});
demoPromise.then(() => {
// This callback will be called when
// the Promise is fulfilled!
})Promises are generic. They don’t “do” anything on their own. When we create a new Promise instance with new Promise(), we also supply a function with the specific asynchronous work we want to do. This can be anything: performing a network request, setting a timeout, whatever.
When that work is finished, we call resolve(), which signals to the Promise that everything went well and resolves the Promise.
Let’s circle back to our original challenge, creating a countdown timer. In that case, the asynchronous work is waiting for a setTimeout to expire.
We can create our own little Promise-based helper, which wraps around setTimeout, like this:
function wait(duration) {
return new Promise((resolve) => {
setTimeout(resolve, duration);
});
}
const timeoutPromise = wait(1000);
timeoutPromise.then(() => {
console.log('1 second later!')
});
This code looks super intimidating. Let’s see if we can break it down it:
- We have a new utility function,
wait. This function takes a single parameter,duration. Our goal is to use this function as a sort ofsleepfunction, but one that works fully asynchronously. - Inside
wait, we’re creating and returning a newPromise. Promises don’t do anything on their own; we need to call theresolvefunction when the async work is completed. - Inside the Promise, we start a new timer with
setTimeout. We’re feeding it theresolvefunction we got from the Promise, as well as thedurationsupplied by the user. - When the timer elapses, it will invoke the supplied callback. This creates a sort of chain reaction:
setTimeoutcallsresolve, which signals that the Promise is fulfilled, which causes the.then()callback to be fired as well.
It’s OK if this code still hurts your brain 😅. We’re combining a lot of hard concepts here! Hopefully the general strategy is clear, even if all the pieces are still a bit fuzzy.
One thing that might help clarify this stuff: in the code above, we’re passing the resolve function directly to setTimeout. Alternatively, we could create an inline function, like we were doing earlier, which invokes the resolve function:
function wait(duration) {
return new Promise((resolve) => {
setTimeout(
() => resolve(),
duration
);
});
}JavaScript has “first class functions”, which means that functions can be passed around like any other data type (strings, numbers, etc). This is a lovely feature, but it can take a while for this to feel intuitive. This alternative form is a bit less direct, but it works exactly the same way, so if this is clearer to you, you can absolutely structure things this way!
Chaining Promises
One important thing to understand about Promises is that they can only be resolved once. Once a Promise has been fulfilled or rejected, it stays that way forever.
This means that Promises aren’t really suitable for certain things. For example, event listeners:
window.addEventListener('mousemove', (event) => {
console.log(event.clientX);
})This callback will be fired whenever the user moves their mouse, potentially hundreds or even thousands of times. Promises aren’t a good fit for this sort of thing.
How about our “countdown” timer scenario? While we can’t re-trigger the same wait Promise, we can chain multiple Promises together:
wait(1000)
.then(() => {
console.log('2');
return wait(1000);
})
.then(() => {
console.log('1');
return wait(1000);
})
.then(() => {
console.log('Happy New Year!!');
});
When our original Promise is fulfilled, the .then() callback is called. It creates and returns a new Promise, and the process repeats.
Passing data
So far, we’ve been calling the resolve function without arguments, using it purely to signal that the asynchronous work has completed. In some cases, though, we’ll have some data that we want to pass along!
Here’s an example using a hypothetical database library that uses callbacks:
function getUser(userId) {
return new Promise((resolve) => {
// The asynchronous work, in this case, is
// looking up a user from their ID
db.get({ id: userId }, (user) => {
// Now that we have the full user object,
// we can pass it in here...
resolve(user);
});
});
}
getUser('abc123').then((user) => {
// ...and pluck it out here!
console.log(user);
// { name: 'Josh', ... }
})Rejected Promises
Unfortunately, when it comes to JavaScript, Promises aren’t always kept. Sometimes, they’re broken.
For example, with the Fetch API, there is no guarantee that our network requests will succeed! Maybe the internet connection is flaky, or maybe the server is down. In these cases, the Promise will be rejected instead of fulfilled.
We can handle it with the .catch() method:
fetch('/api/get-data')
.then((response) => {
// ...
})
.catch((error) => {
console.error(error);
});When a Promise is fulfilled, the .then() method is called. When it is rejected, .catch() is called instead. We can think of it like two separate paths, chosen based on the Promise’s state.
Fetch gotcha
So, let’s say the server sends back an error. Maybe a 404 Not Found, or a 500 Internal Server Error. That would cause the Promise to be rejected, right?
Surprisingly no. In that case, the Promise would still be fulfilled, and the Response object would have information about the error:
Response {
ok: false,
status: 404,
statusText: 'Not Found',
}This can be a bit surprising, but it does sorta make sense: we were able to fulfill our Promise and receive a response from the server! We may not have gotten the response we wanted, but we still got a response.
It checks out, at least by genie-with-three-wishes logic.
When it comes to hand-crafted Promises, we can reject them using a 2nd callback parameter, reject:
new Promise((resolve, reject) => {
someAsynchronousWork((result, error) => {
if (error) {
reject(error);
return;
}
resolve(result);
});
});If we run into problems inside our Promise, we can call the reject() function to mark the promise as rejected. The argument(s) we pass through — typically an error — will be passed along to the .catch() callback.
Confusing names
As we saw earlier, promises are always in one of three possible states: pending, fulfilled, and rejected. Whether a Promise is “resolved” or not is a separate thing. So, shouldn’t my parameters be named “fulfill” and “reject”?
Here’s the deal: the resolve() function will usually mark the promise as fulfilled, but that’s not an iron-clad guarantee. If we resolve our promise with another promise, things get pretty funky. Our original Promise gets “locked on” to this subsequent Promise. Even though our original Promise is still in a pending state, it is said to be resolved at this point, since the JavaScript thread has moved onto the next Promise.
This is something I only learned after publishing this blog post (thanks to the readers who reached out and let me know!), and honestly, I don’t think it’s something 99% of us need to worry about. If you do want to dig even deeper, you can learn more in this document: States and Fates.