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
TLA
- 844