Promises & async/await
Problém: asynchronní funkce vracející hodnoty přes callback nemohou vrátit hodnoty normálním způsobem:
function asyncAdd(a, b, callback) {
setTimeout(() => {
callback(a + b)
}, 1000)
}
const result = asyncAdd(1, 2, (result) => {
return result
})
console.log(result) // undefined
To vede k callback-hell a špatně čitelnému kódu
Promise
Pomise je hodnota která bude vyhodnocena někdy v budoucnosti. Při vytváření potřebuje funkci které se říká handler. První argument promise handleru je funkce (obvykle pojmenovaná resolve) pomocí které vracíme finální hodnotu.
function asyncAdd(a, b) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(a + b)
}, 1000)
})
}
const result = asyncAdd(1, 2)
console.log(result) // Promise { <pending> }
result.then((value) => {
console.log(value) // 3
})
Volání then se dá řetězit
asyncAdd(1, 2)
.then((value) => {
console.log(value) // 3
return asyncAdd(value, value)
})
.then((value) => {
console.log(value) // 6
return asyncAdd(value, 100)
})
.then((value) => {
console.log(value) // 106
})
Nebo uložit vždy do pomocné proměnné
const result1 = asyncAdd(1, 2)
const result2 = result1.then((value) => {
console.log(value) // 3
return asyncAdd(value, value)
})
const result3 = result2.then((value) => {
console.log(value) // 6
return asyncAdd(value, 100)
})
result3.then((value) => {
console.log(value) // 106
})
Návratová hodnota then nemusí být Promise
zasyncAdd(1, 2)
.then((value) => {
console.log(value) // 3
return 4
})
.then((value) => {
console.log(value) // 4
})
Chyby v promisech
Druhý parametr promise handleru je opět funkce (tentokrát ale s typickám nazvem reject) pomocí které můžeme promise zamítnout a vrátit chybu. Ta je následně zachycena pomocí catch. Pokud chceme provést kus kódu nezávisle zda promise vrátil chybu či ne, použijeme finally
function asyncDivide (a, b) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (b === 0) {
reject("nelze dělit nulou")
} else {
resolve(a / b)
}
}, 1000)
})
}
asyncDivide(10, 0)
.then((result) => {
console.log(`Výsledek je: ${result}`)
})
.catch((error) => {
console.error(`Chyba: ${error}`)
})
.finally(() => {
console.log('Děkujeme, že používate naši asynchronní kalkulačku')
})
Pokud je Promise rejectnut ale nemá na sobě catch metodu, Node.js proces se ukončí. Na toto chování pozor u dlouhodobě běžících programů (servery).
function asyncDivide (a, b) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (b === 0) {
reject("nelze dělit nulou")
} else {
resolve(a / b)
}
}, 1000)
})
}
asyncDivide(10, 0)
Čtení & zápis souborů pomocí promisů
import fs from 'fs'
function readFile (path) {
return new Promise((resolve, reject) => {
fs.readFile(path, (err, data) => {
if (err) {
reject(err)
} else {
resolve(data)
}
})
})
}
function writeFile (path, data) {
return new Promise((resolve, reject) => {
fs.writeFile(path, data, (err) => {
if (err) {
reject(err)
} else {
resolve(data)
}
})
})
}
writeFile('hello.txt', 'Hello, World')
.then(() => {
const promise = readFile('hello.txt')
console.log('file written')
return promise
})
.then((data) => {
console.log(data.toString())
})
.catch((err) => {
console.error(err)
})
Node má funkci promisify v balíčku util která nam z callback-style funkcí udělá promise-style funkce
import fs from 'fs'
import util from 'util'
const readFile = util.promisify(fs.readFile)
const writeFile = util.promisify(fs.writeFile)
Nebo můžeme vyuřít balíček fs/promises
import fs from 'fs/promises'
fs.writeFile('hello.txt', 'Hello, World')
.then(() => {
const promise = fs.readFile('hello.txt')
console.log('file written')
return promise
})
.then((data) => {
console.log(data.toString())
})
.catch((err) => {
console.error(err)
})
Async/await
I přesto, že promisy jsou mnohem lepší než callbacky, stále to není ono. JavaScript má tedy dvě klíčová slovíčka, která nam pomohou psát čitelnější kód.
await
Klíčové slovo await nám pomůže extrahovat promise do proměnné bez nutnosti použití then.
import fs from 'fs/promises'
await fs.writeFile('hello.txt', 'Hello, World')
const data = await fs.readFile('hello.txt')
console.log(data.toString())
POZOR: pokud zapomeneme await místo žádané hodnoty dostaneme Promise
async
Klíčové slovo async označuje funkce které vrací Promise. Nemusíme ho tedy vracet manuálně, ale JavaScript to udělá za nás. Zároveň je nutné označit async kařdou funkce která využívá awaitu.
import fs from 'fs/promises'
async function writeAndReadFile(path, text) {
await fs.writeFile(path, text)
const data = await fs.readFile(path)
return data.toString()
}
console.log(await writeAndReadFile('hello.txt', 'Hello, World'))
Async arrow funkce
const writeAndReadFile = async (path, text) => {
// ...
}
Chyby u async/await
Chyby se řeší klasicky pomocí try/catch. Blok finally je opět nepovinný.
import fs from 'fs/promises'
try {
const data = await fs.readFile('neexistujici')
console.log(data)
} catch (err) {
console.error(err)
} finally {
console.log('Tak snad to zafungovalo :)')
}
Sleep - await v cyklu
Await zastaví spuštění kódu (ale neblokuje) a tak můžeme vykonávat asynchronní úkoly sériově
import util from 'util'
const sleep = util.promisify(setTimeout)
for (let i = 0; i < 10; i++) {
console.log(i)
await sleep(1000)
}
console.log('done')
Pro “paralelizaci” použijeme Promise
import util from 'util'
const sleep = util.promisify(setTimeout)
for (let i = 0; i < 10; i++) {
sleep(1000).then(() => {
console.log(i)
})
}
console.log('done')
Perzistentní čítač pomocí async/await
import fs from 'fs/promises'
let count
try {
const data = await fs.readFile('counter.txt')
count = Number(data.toString())
} catch {
count = 0
}
console.log(count)
await fs.writeFile('counter.txt', String(count + 1))
Pro pokročilé
Více promisů zároveň
import fs from 'fs/promises'
const [data1, data2, data3] = await Promise.all([
fs.readFile('file1.txt'),
fs.readFile('file2.txt'),
fs.readFile('file3.txt'),
])
console.log(data1.toString())
console.log(data2.toString())
console.log(data3.toString())
Čtení souboru s timeoutem
import fs from 'fs'
const readFile = (path, timeout = 1000) => new Promise((resolve, reject) => {
const currentTimeout = setTimeout(() => {
reject('operation timed out')
}, timeout)
fs.readFile(path, (err, data) => {
clearTimeout(currentTimeout)
if (err) {
reject(err)
} else {
resolve(data)
}
})
})
try {
const data = await readFile('large.txt', 1)
console.log(data.toString())
} catch (err) {
console.error(err)
}
Awaitovat je možné i objekt s then metodou (thenable)
const thenable = {
then(callback) {
setTimeout(() => {
callback('Hello, World')
}, 1000)
}
}
const msg = await thenable
console.log(msg)