05open 25 min

Concurrency: from Promise orchestration to goroutines and channels

Map JavaScript's async and Promise habits onto Go's goroutines and channels.

by the end of this lesson you can

  • Uses goroutines intentionally
  • Makes coordination explicit
  • Accounts for failure instead of assuming async errors behave like Promise chains

Overview

JavaScript developers already think in terms of the event loop, async work, and Promise orchestration. Go also cares about concurrency, but its tools are goroutines, channels, and explicit coordination patterns rather than Promise chains.

In JavaScript/Node, you often

coordinate concurrent work with Promise combinators and async functions.

In Go, the common pattern is

to launch concurrent work with goroutines and coordinate results explicitly through channels or synchronization tools.

why this difference matters

The key shift is that concurrency in Go feels more like part of ordinary program structure and less like a special async layer.

JavaScript/Node

const user = await fetchUser(id);

Go

ch := make(chan User)
go func() {
    user, _ := fetchUser(id)
    ch <- user
}()
user := <-ch

Deeper comparison

JavaScript/Node version

async function loadDashboard(id) {
  const [user, posts] = await Promise.all([
    fetchUser(id),
    fetchPosts(id),
  ]);
  return { user, posts };
}

Go version

func loadDashboard(id int) (User, []Post, error) {
    userCh := make(chan struct {
        user User
        err  error
    })
    postsCh := make(chan struct {
        posts []Post
        err   error
    })

    go func() {
        user, err := fetchUser(id)
        userCh <- struct {
            user User
            err  error
        }{user: user, err: err}
    }()

    go func() {
        posts, err := fetchPosts(id)
        postsCh <- struct {
            posts []Post
            err   error
        }{posts: posts, err: err}
    }()

    userResult := <-userCh
    if userResult.err != nil {
        return User{}, nil, userResult.err
    }

    postsResult := <-postsCh
    if postsResult.err != nil {
        return User{}, nil, postsResult.err
    }

    return userResult.user, postsResult.posts, nil
}

Reflect

When does Go concurrency feel simpler than Promise-based orchestration, and when does it demand more discipline?

what a strong answer notices

A strong answer mentions lightweight concurrency startup, but also explicit coordination and error handling responsibilities.

Rewrite

Translate this Promise-based JavaScript shape into a Go-style concurrent design.

Rewrite this JavaScript/Node

async function fetchPair(id) {
  const user = await fetchUser(id);
  const posts = await fetchPosts(id);
  return { user, posts };
}

what good looks like

  • Uses goroutines intentionally
  • Makes coordination explicit
  • Accounts for failure instead of assuming async errors behave like Promise chains

Practice

Design a Go function that loads a profile and billing summary concurrently and returns a combined response only when both succeed.

success criteria

  • Explains how results are coordinated
  • Handles at least one failure path clearly
  • Keeps the final result assembly separate from the concurrent fetch logic

Common mistakes

  • Expecting channels to behave like Promises or streams with the same ergonomics.
  • Adding goroutines before the sequential version is clear.
  • Ignoring cleanup and failure coordination because Promise helpers used to hide more of it.

takeaways

  • The key shift is that concurrency in Go feels more like part of ordinary program structure and less like a special async layer.
  • A strong answer mentions lightweight concurrency startup, but also explicit coordination and error handling responsibilities.
  • Explains how results are coordinated