Concurrency in TypeScript: Threads, Goroutines, and async/await — A Practical Guide

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Concurrency in TypeScript: From Event Loop to Web Workers

\n\n

Have you ever noticed how your browser or Node.js application "freezes" during a complex mathematical operation or when processing a large JSON? That familiar feeling of helplessness when the interface stops responding to clicks and the server stops responding to new requests. The reason is that by default, JavaScript (and its strict sibling TypeScript) is a single-threaded language. But this doesn't mean we are doomed to slow programs. On the contrary, proper use of concurrency in TypeScript allows you to create fast and responsive applications.

\n\n

In this article, we won't just list terms. We'll break down how async/await actually works, what lies under the hood of the Event Loop, whether you can use "goroutines" in TypeScript like in Go, and when to use real threads via Web Workers. Let's go!

\n\n

1. Asynchrony vs Parallelism: What's the Difference?

\n\n

Before diving into the code, let's agree on terms. Many people confuse these concepts, and that's a mistake.

\n\n
    \n
  • Asynchrony (Concurrency) — the ability of a program to start a task, switch to another while the first one is waiting (e.g., for a server response), and return to it later. Only one instruction is executed at any given moment.
  • \n
  • Parallelism — the execution of two or more tasks at literally the same time on different CPU cores.
  • \n
\n\n

TypeScript gives us asynchrony out of the box. Parallelism, however, requires creating separate threads — in the browser this is Web Workers, in Node.js — worker_threads. Remember this difference, otherwise you risk writing code that "lags" due to blocking the main thread.

\n\n

2. Event Loop and Callbacks — The Foundation of TypeScript Concurrency

\n\n

The main secret of asynchrony in JavaScript/TypeScript is the Event Loop. Imagine you have a task queue. The main thread picks up a task, executes it — if it's synchronous, it immediately returns the result. If it's an asynchronous operation (e.g., setTimeout or fetch), it gets sent "aside" (to Web API or libuv), and the main thread continues executing the next code.

\n\n

When the asynchronous operation completes, its callback goes into a queue (Task Queue or Microtask Queue). The Event Loop constantly checks if the call stack is empty, and if it is, it takes the first task from the queue and executes it.

\n\n
// A simple example demonstrating this\nconsole.log('1. Start');\n\nsetTimeout(() => {\n    console.log('3. Timer fired');\n}, 0);\n\nPromise.resolve().then(() => {\n    console.log('2. Microtask executed');\n});\n\nconsole.log('4. End');\n\n// Output: 1, 4, 2, 3
\n\n

Notice that the Promise (microtask) executed before the setTimeout (macrotask), even though the timer delay is 0 ms. This is important knowledge for TypeScript optimization.

\n\n

3. Async/Await — Sugar That Changes Everything

\n\n

With the advent of ES2017, the JS world split into "before" and "after". Async/await is syntactic sugar over promises that makes asynchronous code look synchronous. It doesn't add threads, but it radically improves readability.

\n\n
// Function simulating a delay\nfunction delay(ms: number): Promise<void> {\n    return new Promise(resolve => setTimeout(resolve, ms));\n}\n\n// Async function using async/await\nasync function fetchUserData(userId: number): Promise<string> {\n    console.log(`Requesting data for user ${userId}...`);\n    await delay(2000); // Simulating a network request\n    return `Data for user ${userId}`;\n}\n\nasync function main() {\n    console.log('Start');\n    const user1 = await fetchUserData(1);\n    console.log(                

Blogs

Book Recommendations