TypeScript Runs on One Thread — Completion Is the Part That’s Out of Order
Disclaimer: This post is aimed at developers who are newer to TypeScript and are still building an intuition for asynchronous code. If async and await have ever felt unintuitive, this post is for you.
Your Code isnt meant to run in order
Consider the following code
async function task(name: string, delay: number) {
console.log(`start ${name}`);
await new Promise(resolve => setTimeout(resolve, delay));
console.log(`end ${name}`);
}
async function main() {
task("A", 100);
task("B", 100);
}
main();On one run the output might look like this
start A
start B
end A
end BRunning the code again, you might get a different output like this
start A
start B
end B
end AWhen I first started out learning JS/TS, this confused me. How can the same code yield different outputs? Won't that make your code non-deterministic?
Is this Behavior JavaScript-Specific?
No. this behavior isn’t unique to JavaScript or TypeScript. In many programming languages, it’s common to start multiple operations that complete later: network requests, file I/O, timers, or database queries. While these operations may be initiated in a fixed order, the order in which they finish depends on factors outside the program itself, such as operating system scheduling or external systems.
What makes JavaScript feel different is not that it behaves uniquely, but that it makes this behavior explicit. JavaScript runs on a single execution thread, so asynchronous work cannot “run in the background” on the same thread. Instead, that work is delegated to the environment and scheduled to resume later.
If you are new to JS/TS, this post is meant to help you wrap your head around the concept of async or await
So how are you supposed to make your code run "in order"?
Promises exist to solve this problem. Instead of blocking, JavaScript represents future results using promises — values that are not available yet, but will be at some point. This allows the program to continue executing while asynchronous work is handled elsewhere. So what are promises?
You can think of promises like receipts for work that has been started but not finished yet. When a function returns a promise, it’s not giving you the result — it’s giving you a guarantee that the result will eventually arrive. The promise itself is a value you can pass around, store, or return, even though the actual data isn’t available yet. At some point in the future, the promise will either be fulfilled with a value or rejected with an error, and only then can dependent code continue.
function functionThatReturnsPromise(): Promise<number> {
return new Promise(resolve => {
setTimeout(() => {
resolve(42);
}, 1000);
});
}
const result = functionThatReturnsPromise();
console.log(result);The result of executing the code above in the browser console would look something like this:
Promise { <state>: "pending" }So How Do You Get the Value Out of a Promise?
At this point, we know that calling an asynchronous function does not return the result itself — it returns a promise representing a future result. Logging that promise immediately shows that it is still pending.
So how do you actually access the value?
The naive approach is to treat the promise like the value it will eventually hold:
const result = functionThatReturnsPromise();
console.log(result + 1);This doesn’t work because result is not a number. It’s a promise. The value doesn’t exist yet.
To wait for the promise to settle without blocking the entire program, JavaScript provides the await keyword.
async function main() {
const result = await functionThatReturnsPromise();
console.log(result);
}
main();This time, the output is:
42When the promise resolves, execution resumes exactly where it left off, with the resolved value assigned to result.
So What Should Be an async Function?
At this point, it’s clear that async functions exist to work with values that are not available immediately. The question then becomes: when should a function actually be marked as async?
A good rule of thumb is this:
A function should be
asyncif it depends on work that completes later
Examples include:
- network requests (HTTP calls, API requests)
- file system operations
- database queries
- timers or delays
- any other I/O-bound operation
These operations are inherently asynchronous. They rely on external systems such as the network, disk, or operating system.
If a function awaits another async function, it must itself be async. This creates a clear boundary where asynchrony enters your program and flows upward through the call stack.
If its one thing to take away, remember: You await an async function that returns a promise.