Top level await

The Footgun in your pocket

Nikita Malyschkin
@nmalyschkin

Nikita Malyschkin

web developer by day
philosopher when drunk

 

Javascript / Python / C++ / more

 

Twitter / GitHub / Telegram

@nmalyschkin

History of Async Programming in Javascript

CALLBACKS

Promises

ASYNC/AWAIT

CALLBACKS

Promises

ASYNC/AWAIT

import { readFile, writeFile } from "fs";

const main = () => {
    // copy file by read and write
};

main();
import { readFile, writeFile } from "fs";

const main = () => {
    readFile("./my/file");
};

main();
import { readFile, writeFile } from "fs";

const main = () => {
    readFile("./my/file", (err, data) => {
      /* do something here */
    });
};

main();
import { readFile, writeFile } from "fs";

const main = () => {
    readFile("./my/file", (err, data) => {
        if (err) {
            console.log(err);
            return;
        }
      
        // write data to new file
    });
};

main();
import { readFile, writeFile } from "fs";

const main = () => {
    readFile("./my/file", (err, data) => {
        if (err) {
            console.log(err);
            return;
        }

        writeFile("./my/fileCopy", data);
    });
};

main();
import { readFile, writeFile } from "fs";

const main = () => {
    readFile("./my/file", (err, data) => {
        if (err) {
            console.log(err);
            return;
        }

        writeFile("./my/fileCopy", data, err => {
            if (err) {
                console.log(err);
                return;
            }

            console.log("file copied");
        });
    });
};

main();

CALLBACKS

Promises

ASYNC/AWAIT

import fs from "fs";
import util from "util";

const readFile = util.promisify(fs.readFile);
const writeFile = util.promisify(fs.writeFile);

const main = () => {
    // copy file by read and write
};

main();
import fs from "fs";
import util from "util";

const readFile = util.promisify(fs.readFile);
const writeFile = util.promisify(fs.writeFile);

const main = () => {
    readFile("./my/file")
};

main();
import fs from "fs";
import util from "util";

const readFile = util.promisify(fs.readFile);
const writeFile = util.promisify(fs.writeFile);

const main = () => {
    readFile("./my/file")
        .then(data => {/* do something here */})
};

main();
import fs from "fs";
import util from "util";

const readFile = util.promisify(fs.readFile);
const writeFile = util.promisify(fs.writeFile);

const main = () => {
    readFile("./my/file")
        .then(data => writeFile("./my/fileCopy"))
        .then(() => console.log("file copied"))
};

main();
import fs from "fs";
import util from "util";

const readFile = util.promisify(fs.readFile);
const writeFile = util.promisify(fs.writeFile);

const main = () => {
    readFile("./my/file")
        .then(data => writeFile("./my/fileCopy"))
        .then(() => console.log("file copied"))
        .catch(err => console.log(err));
};

main();
import fs from "fs";
import util from "util";

const readFile = util.promisify(fs.readFile);
const writeFile = util.promisify(fs.writeFile);

const main = () => {
    readFile("./my/file")
        .then(data => writeFile("./my/fileCopy"))
        .then(() => console.log("file copied"))
        .catch(err => console.log(err))
        .finally(() => console.log("done"));
};

main();

CALLBACKS

Promises

ASYNC/AWAIT

import fs from "fs";
import util from "util";

const readFile = util.promisify(fs.readFile);
const writeFile = util.promisify(fs.writeFile);

const main = () => {
    // copy file by read and write
};

main();
import fs from "fs";
import util from "util";

const readFile = util.promisify(fs.readFile);
const writeFile = util.promisify(fs.writeFile);

const main = async () => {
    // copy file by read and write
};

main();
import fs from "fs";
import util from "util";

const readFile = util.promisify(fs.readFile);
const writeFile = util.promisify(fs.writeFile);

const main = async () => {
    const data = await readFile("./my/file");
};

main();
import fs from "fs";
import util from "util";

const readFile = util.promisify(fs.readFile);
const writeFile = util.promisify(fs.writeFile);

const main = async () => {
    const data = await readFile("./my/file");
    await writeFile(".my/fileCopy", data);
};

main();
import fs from "fs";
import util from "util";

const readFile = util.promisify(fs.readFile);
const writeFile = util.promisify(fs.writeFile);

const main = async () => {
    const data = await readFile("./my/file");
    await writeFile(".my/fileCopy", data);
    console.log("file copied");
};

main();
import fs from "fs";
import util from "util";

const readFile = util.promisify(fs.readFile);
const writeFile = util.promisify(fs.writeFile);

const main = async () => {
    try {
        const data = await readFile("./my/file");
        await writeFile(".my/fileCopy", data);
        console.log("file copied");
    } catch (error) {
        console.log(error);
    }
    console.log("done");
};

main();

ES Modules

.js

.mjs

const fs = require("fs")
import fs from "fs"

ES Modules

Babel Modules

ES Modules does not include

  • require
  • exports
  • module.exports
  • __filename
  • __dirname

TOP LEvel AWAit

TOP LEvel AWAit

(in modules)

import fs from "fs";
import util from "util";

const readFile = util.promisify(fs.readFile);
const writeFile = util.promisify(fs.writeFile);

const main = async () => {
    const data = await readFile("./my/file");
    await writeFile(".my/fileCopy", data);
    console.log("file copied");
};

main();

import fs from "fs";
import util from "util";

const readFile = util.promisify(fs.readFile);
const writeFile = util.promisify(fs.writeFile);

(async () => {
    const data = await readFile("./my/file");
    await writeFile(".my/fileCopy", data);
    console.log("file copied");
})();

import fs from "fs";
import util from "util";

const readFile = util.promisify(fs.readFile);
const writeFile = util.promisify(fs.writeFile);

// with top level await
const data = await readFile("./my/file");
await writeFile(".my/fileCopy", data);
console.log("file copied");

import fs from "fs";
import util from "util";

const readFile = util.promisify(fs.readFile);
const writeFile = util.promisify(fs.writeFile);

// with top level await
const data = await readFile("./my/file");
await writeFile(".my/fileCopy", data);
console.log("file copied");

so, ARE we done Here?

What if I import a Module with top Level Await?

// module.js

let string = "not done";

(async () => {
    string = await new Promise(res => {
        setTimeout(() => {
            res("done");
        }, 1000);
    });
})();

module.exports = string;
//main.js

const string = require("./module.js")

console.log(string);

setTimeout(() => {
    console.log(string)
}, 1500);

CommonJS

// module.js

let string = "not done";

(async () => {
    string = await new Promise(res => {
        setTimeout(() => {
            res("done");
        }, 1000);
    });
})();

module.exports = string;
//main.js

const string = require("./module.js")

console.log(string); // "not done"

setTimeout(() => {
    console.log(string) // "not done"
}, 1500);

CommonJS

// module.mjs

export let string = "not done";

(async () => {
    string = await new Promise(res => {
        setTimeout(() => {
            res("done");
        }, 1000);
    });
})();

// main.mjs

import { string } from "./module.mjs";

console.log(string); 

setTimeout(() => {
    console.log(string);
}, 1500);

ES Modules

// module.mjs

export let string = "not done";

(async () => {
    string = await new Promise(res => {
        setTimeout(() => {
            res("done");
        }, 1000);
    });
})();

// main.mjs

import { string } from "./module.mjs";

console.log(string); // "not done"

setTimeout(() => {
    console.log(string); // "done"
}, 1500);

ES Modules

// module.mjs

export let string = "not done";


string = await new Promise(res => {
    setTimeout(() => {
        res("done");
    }, 1000);
});

// main.mjs

import { string } from "./module.mjs";

console.log(string);

setTimeout(() => {
    console.log(string);
}, 1500);

ES Modules with TLA

// module.mjs

export let string = "not done";


string = await new Promise(res => {
    setTimeout(() => {
        res("done");
    }, 1000);
});

// main.mjs

import { string } from "./module.mjs";

console.log(string); // "done"

setTimeout(() => {
    console.log(string); // "done"
}, 1500);

ES Modules with TLA

// module.mjs

export let string = "not done";


string = await new Promise(res => {
    setTimeout(() => {
        res("done");
    }, 1000);
});

// main.mjs

import { string } from "./module.mjs";

console.log(string); // "done"

setTimeout(() => {
    console.log(string); // "done"
}, 1500);

ES Modules with TLA

So, What the hell Happens?

// module.mjs

export let string = "not done";


string = await new Promise(res => {
    setTimeout(() => {
        res("done");
    }, 1000);
});

// main.mjs

import { string } from "./module.mjs";

console.log(string); // "done"

setTimeout(() => {
    console.log(string); // "done"
}, 1500);

ES Modules with TLA

// module.mjs

export let string = "not done";


    string = await new Promise(res => {
        setTimeout(() => {
            res("done");
        }, 1000);
    });

ES Modules with TLA

ES Modules with TLA

// module.mjs

export let string = "not done";

export const promise = (async () => {
    string = await new Promise(res => {
        setTimeout(() => {
            res("done");
        }, 1000);
    });
})();

ES Modules with TLA

// main.mjs

import { string } from "./stringModule.mjs";
import { number } from "./numberModule.mjs";


console.log(string);

setTimeout(() => {
    console.log(string);
}, 1500);

ES Modules with TLA

// main.mjs

import { promise as p1, string } from "./stringModule.mjs";
import { promise as p2, number } from "./numberModule.mjs";

export const promise = Promise.all([p1, p2]).then(async () => {
    console.log(string);

    setTimeout(() => {
        console.log(string);
    }, 1500);
})

Use cases for

Top Level Await

Dynamic dependency pathing

const strings = await import(`/i18n/${navigator.language}`);

Resource initialization

const connection = await dbConnector();

Dependency fallbacks

export let jQuery;
try {
    jQuery = await import('https://cdn-a.com/jQuery');
} catch {
    jQuery = await import('https://cdn-b.com/jQuery');
}

Dependency fallbacks

export let jQuery;
try {
    jQuery = export const jQuery = Promise.race([
      	import('https://fastcdn-a.com/jQuery'),
      	import('https://fastcdn-b.com/jQuery')
    ]);
} catch {
    jQuery = await import('https://slowcdn.com/jQuery');
}

Now let's turn

TOP LEVEL AWAIT

into a footgun

// main.mjs
import a from "a.mjs"

startApp(a);


// a.mjs
import b from "b.mjs"

const resources = await fetch("/a-resources.json");
export default A(resources, b);


// b.mjs
import c from "c.mjs"

const resources = await fetch("/b-resources.json");
export default B(resources, c);


// c.mjs
const resources = await fetch("/c-resources.json");
export default C(resources);

C

B

A

Main

How Can we do better?

// main.mjs
import a from "a.mjs"

startApp(a);


// a.mjs
import b from "b.mjs"
import resources from "aResources.mjs"

export default A(resources, b);

// aResources.mjs
export default await fetch("/a-resources.json");

// [...]

C

B

A

Main

DON'T WE HAVE
THE SAME PROBLEM
WITH COMMONJS?

DON'T WE HAVE
THE SAME PROBLEM
WITH COMMONJS?

No!
CommonJS does not give the possibility to await asynchronous tasks before exporting in any way.

Imperative imports
with TOP level Await

// main.mjs
import a from "a.mjs" // <- declarative

const b = await import("b.mjs") // <- imperative
main();

// a.mjs
const aPromise = await fetch("/a")
export default aPromise;

// b.mjs
const bPromise = await fetch("/b")
export default bPromise;

B

A

Main

B

A

Main

RULE of THUmb

Don't mix imperative and declarative imports

WITH GREAT POWER

COMes great Responsibility

TLA

By Nikita Malyschkin