fetch((err, res) => {
if (err) {
// yay - error handling! :)
}
const myJson = res.json();
// do stuff
}
What's so unreadable about this? Even if it is 4 calls it would still be readable because if you have any sizeable amount of logic you should be refactoring it into several functions anyway with the above function gluing them together. Much easier to write tests for too! fetch(url).then( response => {
response.json().then( json => {
// do stuff
});
});
This is already much worse then the await version, without any error handling yet. And if I need to call another promise in order, I'd need yet another level of callback. Instead of two easily intelligible lines that could be wrapped into a single try/catch, we now have 4 lines and two new scopes/levels.Why would I ever use this over await?
fetch(url)
.then((response) => response.json())
.then((json) => {
// do stuff
})
Promises will wait until resolved, normal values will call the next handler "right away" (there's some nuance here and some edge cases about what "right away" means, but for the most part you never need to think about that)And if you want to do more things and handle errors, it becomes pretty simple as well:
fetch(url)
.then((response) => response.json())
.then((json) => {
if (!json.user.id) {
throw new Error('user id not found')
} else {
return db.sql('SELECT * from users where id = $1', [json.user.id])
}
}).then((user) => {
return response.send(user)
}).catch((err) => {
// any error thrown at any point during the chain will trigger this catch
return response.error(err)
})
I still completely agree that async/await is still better in this case, but then throw some more wrenches into the situation like wanting to handle multiple promises at a time and you start to see where using "raw promises" really comes in handy. Like this: try {
const res = await fetch(url)
const json = await res.json()
if (!json.user.id) {
throw new Error('user id not found')
}
const [ userObj, userAuthLevel, someOtherStuff] = await Promise.all([
db.sql('SELECT * from users where id = $1', [json.user.id]),
db.sql('SELECT * from otherStuff where userId = $1', [json.user.id]),
fetch('https://other.stuff/and/things')
])
return response.send({
userObj,
userAuthLevel,
someOtherStuff
})
} catch (err) {
return response.error(err)
}
or say there's an expensive call that you can start BEFORE the first fetch, but still need to wait on later (ignoring most other stuff for simplicity): // notice there's no await...
// we can kick off the request now, but not wait for the result until later
const someOtherStuffPromise = fetch('https://other.stuff/and/things')
const res = await fetch(url)
const json = await res.json()
// do other things here
// finally wait for the promise to resolve here.
const someOtherStuff = await someOtherStuffPromiseEither way, there's a very interested construct with promises I didn't realize was possible, but makes sense now that I see it. I never thought about awaiting an initiated promise at any point later on.
fetch((err, res) => {
if (err) {
// yay - error handling! :)
}
res.json((err, myJson) => {
if (err) {
// oh no - another error handler :(
}
// do stuff
});
}
fetch.json is async as well. With await the try / catch would just be around both await calls.I don't actually have a problem with the callback version because it is nice and explicit. But again, I did originally say say that "async/await" was actually a valid use case in this necessarily serial scenario (and is exactly what I do in my article that I linked to). ;)
That said, if I see a chain of serial calls more than a few levels deep it is a code smell that indicates refactoring is necessary. There really is no reason to write code like that unless it's very simple logic. This is why I have never had a problem with "callback hell" even though I write code that is heavily asynchronous (APIs, WebSocket, pub/sub etc.)