Användning av await - asynkron programkod
6 minuter i lästid JavaScript Nodejs Async

Användning av await - asynkron programkod

Den här artikeln tar vid direkt efter där förra veckans artikel slutade. Det vi diskuterade förra vecka var begreppet promise och hur du använder det i JavaScript. Den första artikeln i denna serie diskuterade call-back funktioner och de problem med nästlade funktioner detta för med sig.

Emellertid, var de först med begreppet await, som asynkron programkod kunde formuleras på ett både enkelt och begripligt sätt. JavaScript fick stöd för async funktioner med await i ES2017. Funktioner med await var så pass efterlängtat att det tog bara några månader förrän detta var implementerat i alla moderna webbläsare. I och med att både Google Chrome och Node.js bygger V8 (JS Engine), så blev det tillgängligt för programkod på serversidan också.

Hur använder man då await?

I stället för att anropa .then() på ett promise objekt, skriver man await framför i stället.

Kodexempel med promise objekt

fetch('https://www.ribomation.se/')
    .then(response => response.text())
    .then(html => console.log(html));

Kodexempel med await

const response = await fetch('https://www.ribomation.se/');
const html     = await response.text();
console.log(html);

I koden ovan, gör vi ett HTTP anrop med fetch(), som returnerar ett promise objekt, vilket vi sen får ett nytt promise objekt av via funktionen .text() som returnerar själva innehållet (eng. response body).

Async funktioner

Inledningsvis, var man tvungen att använda await i en funktion markerad som async och sen anropa denna.

async function main() {
    const response = await fetch('https://www.ribomation.se/');
    const html     = await response.text();
    console.log(html);
}

main();

Eller, allt i ett svep som en async IIFE (Immediately Invoked Function Expression).

(async () => {
    const response = await fetch('https://www.ribomation.se/');
    const html     = await response.text();
    console.log(html);
})();

Några år senare, med ES2020, kom top-level await som medgav att man kunde anropa await direkt i huvudprogrammet, såsom det första kodexemplet med await illustrerade.

Historik

JavaScript var ingalunda först med att införa await som ett sätt att sekventiellt ordna anrop till continuation funktioner. Själva konceptet dök upp i det funktionella språket ML i början på 1990-talet med implementerades och blev populärt i det senare funktionella språket Haskel i slutet av 1990-talet.

När Microsoft implementerade sitt eget funktionella språk F# en bit in på 2000-talet, baserat primärt på ML med viss mix från Haskel, så blev det en del av vad de kallade asynchronous workflows. Från F#, blev steget till C# kort och async/await infördes där av Anders Hejlsberg 2012. I och med att begreppet diskuterades vid den tiden för ES, så var Anders kvick med att implementera det i TypeScript också några år senare.

I C++ har await helt nyligen dykt upp i och med standarden C++20 börjar bli implementerad. Det utgör en del av stödet för coroutines och representeras av det nya reserverade order co_await.

auto my_coroutine(std::string url) -> Task<std::string> {
    auto response = co_await fetch_cxx(url);
    auto text     = co_await response.extract_body_as_text();
    std::cout << text << std::endl;
}

int main() {
    std::string url = "https://www.ribomation.se/"s;
    auto task = my_coroutine( std::move(url) );
}

Hantering av fel

I förra veckan, såg vi hur man hanterade fel från promise objekt, via kaskadfunktionen .catch(). Med await blir det betydligt enklare och mer begripligt.

Om vi börjar med hur det ser ut när man använder ett promise objekt.

fetch('https://www.ribomation.se/')
    .then(response => response.text())
    .then(html => console.log(html))
    .catch(err => console.error(err));

Går vi sen över till användning av await, så blir det enklare med den familjära try-catch satsen (vilken Bjarne Stroustrup uppfann i slutet på 1980-talet för att kunna kapsla in setjmp/longjmp anrop när han införde exceptions i språket).

try {
    const response = await fetch('https://www.ribomation.se/');
    const html     = await response.text();
    console.log(html);
} catch (err) {
    console.error(err);
}

Kopiera en fil

I min första artikel i denna serie, illustrerade jag begreppet call-back hell med en funktion som i Node.js kopierar innehållet i en textfil till en ny fil och transformerar texten till versaler. Här kommer den funktionen på nytt, så vi kan följa evolutionsstegen.

function copyfile(fromFile, toFile) {
    open(fromFile, 'r', (err, fd) => {
       if (err) die(err);
       fstat(fd, (err, stats) => {
           if (err) die(err);
           const storage = Buffer.alloc(stats.size);
           read(fd, storage,0, storage.length,null, (err, bytesIn, data) => {
               if (err) die(err);
               console.log('Read  %d bytes from %s', bytesIn, basename(fromFile));
               close(fd, (err) => {
                   if (err) die(err);
                   open(toFile, 'w', (err, fd) => {
                       if (err) die(err);
                       const payload = data.toString().toUpperCase();
                       write(fd, payload, (err, bytesOut) => {
                           if (err) die(err);
                           close(fd, (err) => {
                               console.log('Wrote %d bytes to %s', bytesOut, basename(toFile));
                           });
                       });
                   });
               });
           });
       })
    });
}

I min andra artikel, skrev jag om funktionen till att använda en promise-kedja på följande vis.

function copyfile(fromFile, toFile) {
    const state = {};
    return open(fromFile, 'r')
        .then(fd => {
            state.fd = fd;
            return fstat(fd);
        })
        .then(stats => {
            const storage = Buffer.alloc(stats.size);
            return read(state.fd, storage, 0, storage.length, null);
        })
        .then(response => {
            console.log('Read  %d bytes from %s', response.bytesRead, basename(fromFile));
            state.text = response.buffer.toString();
            return close(state.fd);
        })
        .then(() => {
            return open(toFile, 'w');
        })
        .then(fd => {
            state.fd = fd;
            const payload = state.text.toString().toUpperCase();
            return write(fd, payload);
        })
        .then(response => {
            state.bytesWritten = response.bytesWritten;
            return close(state.fd);
        })
        .then(() => {
            console.log('Wrote %d bytes to %s', state.bytesWritten, basename(toFile));
        })
        .catch(err => {
            console.error('*** %s', err);
        });
}

Till slut, kan vi nu skriva om funktionen till att vara async. Notera nedan, hur vi jag använder try-catch-finally.

async function copyfile(fromFile, toFile) {
    console.time('Elapsed');
    try {
        let   fd       = await open(fromFile, 'r');
        const stats    = await fstat(fd);
        const storage  = Buffer.alloc(stats.size);
        let   response = await read(fd, storage, 0, storage.length, null);
        await close(fd);
        console.log('Read  %d bytes from %s', response.bytesRead, basename(fromFile));
        
        const text    = response.buffer.toString();
        const payload = text.toString().toUpperCase();
        fd            = await open(toFile, 'w');
        response      = await write(fd, payload);
        await close(fd);
        console.log('Wrote %d bytes to %s', response.bytesWritten, basename(toFile));
    } catch (err) {
        console.error('failed: %o', err)
    } finally {
        console.timeEnd('Elapsed')
    }
}

Hela programmet ser ut så här:

import {fileURLToPath} from 'node:url';
import {basename, dirname, extname, join} from 'node:path';
import fs from 'node:fs';
import {promisify} from 'node:util'

const open  = promisify(fs.open);
const fstat = promisify(fs.fstat);
const read  = promisify(fs.read);
const write = promisify(fs.write);
const close = promisify(fs.close);

async function copyfile(fromFile, toFile) { /*...*/ }

const scriptFilePath = process.argv[2] || fileURLToPath(import.meta.url);
const scriptFileName = basename(scriptFilePath, extname(scriptFilePath));
const outputFilePath = join(dirname(scriptFilePath), scriptFileName + '-copy.txt');

await copyfile(scriptFilePath, outputFilePath);

Samt, ett körexempel här:

$ node copyfile-await.mjs 
Read  1480 bytes from copyfile-await.mjs
Wrote 1480 bytes to copyfile-await-copy.txt
Elapsed: 10.889ms

Länkar