Cutting Through the Noise: Asynchronous JavaScript, Stripped Bare

In the trenches of code, clarity saves time. Let’s talk straight about asynchronous JavaScript. No fluff, just the bare bones.

Concurrency isn’t parallelism. Understand this first. Parallelism is about doing lots of things at the same time. Got a big machine? It can run your code on different tracks, like trains on parallel rails. That’s parallelism.

But JavaScript, it’s a different beast. It does one thing at a time, one line after the other. That’s single-threaded, that’s its turf. Yet, it handles tasks that wait on other things, like a response from a server far away. It doesn’t like to sit idle. It does other jobs in the meantime. That’s asynchronicity.

Now, meet the workhorse of asynchronicity: callbacks. They tell JavaScript, “When you’re done with that task, do this.” But they’re not perfect. They get messy, tangled. People call it “callback hell.” It’s not about messy lines of code. It’s deeper. It’s about losing sight of what happens when, and what follows what.

// Callback example: A simple treasure hunting game
// Note: It's great when it's simple like this, but things can quickly get nasty
// and hard to predict when they're deeply nested.
function seekTreasure(callback) {
  console.log('🏴‍☠️ Searching for treasure...')
  setTimeout(function () {
    callback('💎 Found the hidden treasure!')
  }, 2000)
}

seekTreasure(function (result) {
  console.log(result) // Outputs: "💎 Found the hidden treasure!" after 2 seconds
})

Enter thunks. They’re the unsung heroes, making a bit of sense of this mess. They’re a stepping stone to promises, cutting through the chaos.

// Thunk example: Delaying a space mission launch announcement
function createLaunchAnnouncementThunk() {
  let missionDetails = "🚀 Mission 'Red Planet' will launch in 3... 2... 1... 🌕"
  return function delayedAnnouncement(callback) {
    console.log('👩‍🚀 Preparing the announcement...')
    setTimeout(() => {
      callback(missionDetails) // The announcement happens after a delay
    }, 2000)
  }
}

// Creating our thunk
const launchAnnouncementThunk = createLaunchAnnouncementThunk()

// Sometime later in the program, we decide to execute the thunk
console.log("🌌 Everyone is ready. Let's make the announcement!")
launchAnnouncementThunk((message) => {
  console.log(message) // Outputs: "🚀 Mission 'Red Planet' will launch in 3... 2... 1... 🌕" after 2 seconds
})

Promises are the next breed. They deal with the mess, saying, “I might not have the result now, but I promise to handle it.” They make the code cleaner, sure. But they’re not a golden hammer. They’re a tool, good for some jobs, not all.

// Promise example: An adventure through space
function launchRocket() {
  console.log('🚀 Preparing for launch...')
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve('🌟 Blast off! Entering the cosmos!')
    }, 4000)
  })
}

launchRocket().then((result) => {
  console.log(result) // Outputs: "🌟 Blast off! Entering the cosmos!" after 4 seconds
})

Beyond promises, generators wait. They pause and resume tasks, making asynchronous code behave like well-behaved, synchronous code. They make things simpler, not easy.

// Generator example: An asynchronous journey through time
// Note: Generators and promises are the under-the-hood backbone of the
// async-await sugar syntax
function delay(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms))
}

function* timeTravel() {
  console.log('🕰 Charging the time machine...')
  yield delay(2000) // Charging takes 2 seconds
  console.log('🦖 Jumping to the Jurassic period!')

  yield delay(3000) // Wait 3 seconds before the next jump
  console.log('🏰 Fast-forwarding to the Medieval times!')

  yield delay(2500) // Wait 2.5 seconds before the final jump
  console.log('🚀 Whoosh! Welcome to the Future!')
}

// We need a function to run our generator and handle the promises it yields
async function runTimeTravel() {
  const journey = timeTravel()

  // The generator gives us promises. We'll wait for each to complete before resuming the journey.
  for await (const _ of journey); // We don't need the results, just the delays
}

runTimeTravel()

Then, there’s the talk of the town, observables. They handle streams of events, not just single outcomes. They’re good listeners, reacting to sequences of events over time. Yet, they’re not the end of the story.

// Observable example (using RxJS library): Communicating with aliens
import { Observable } from 'rxjs'

const alienCommunication = new Observable((subscriber) => {
  console.log('👽📡 Initiating communication with aliens...')
  subscriber.next('👽 Greetings, Earthling!')
  setTimeout(() => {
    subscriber.next('👽 We come in peace!')
  }, 5000)
})

alienCommunication.subscribe((message) => {
  console.log(message) // Outputs: "👽 Greetings, Earthling!" immediately, then "👽 We come in peace!" after 5 seconds
})

CSP, or communicating sequential processes, is an old dog with new tricks. It juggles events between different places in your code. It’s not famous, but it’s powerful. It’s like a seasoned conductor making an orchestra out of solo artists.

// CSP example: Implementing secret superhero communication using js-csp
// Note: True CSP with parallelism is theoretically possible with workers, but no
// serious groundwork has yet to take off in integrating an all-in-one solution.
// Thus' you'd need to handle the workers part either with a library or yourself.
// CSP is Go's async model!
import csp from 'js-csp'

async function superheroCommunication() {
  // Create a new channel for the superheroes to communicate
  const channel = csp.chan()

  // Superhero A sends a message
  csp.go(function* () {
    console.log('🦸‍♂️ Superhero A: Sending a secret message...')
    yield csp.put(channel, '🔒 Meet at the secret base. - Superhero A')
  })

  // Superhero B receives the message
  csp.go(function* () {
    console.log('🦸‍♀️ Superhero B: Waiting for the message...')
    const message = yield csp.take(channel)
    console.log(`🦸‍♀️ Superhero B received: ${message}`)
  })

  // Close the channel after the communication
  setTimeout(() => {
    channel.close()
    console.log('🔇 Channel closed.')
  }, 3000)
}

superheroCommunication()

Here’s the truth: there’s no one-fixes-all in asynchronous JavaScript. Callbacks, thunks, promises, generators, observables, CSP—they’re all just tools in your belt. Use them with reason.

Don’t get lost in the noise. Writing code isn’t about following trends. It’s about solving problems. Pick the right tool for the job. Make your code work for you, not the other way around.

Keep it simple. Keep it clear. That’s code at its best.