If you've been following these tutorials, you know that spawn and channels are powerful tools. But let's be real—sometimes, you don't want to build a whole pipe system just to get a single answer back from a background task. Sometimes, you just want to say "do this, and tell me when you're done."
That's where async and await come in. It’s a cleaner, higher-level layer on top of our existing concurrency primitives.
Running a task asynchronously
When you prefix a call with async, you're telling the compiler, "go run this in the back office." It immediately hands you a future—a placeholder that promises to hold the result once the work is finished.
module main
fn compute(id: int) -> int {
print("Task {id} starting...")
return id * 10
}
fn main() -> int {
// These start immediately, no waiting required.
f1 := async compute(1)
f2 := async compute(2)
print("Main thread is busy doing other things...")
// Now we stop and collect our homework.
res1 := await f1
res2 := await f2
print("Task 1 returned: {res1}")
print("Task 2 returned: {res2}")
return 0
}
It’s tidy, it’s readable, and it saves you from the "spaghetti channel" nightmare.
Returning multiple values
Life isn't always single-value perfection. await plays nicely with tuples, too.
fn fetch(id: int) -> (string, bool) {
return "data-{id}", true
}
fn main() -> int {
f := async fetch(1)
// await unpacks the tuple just like a standard function call
data, ok := await f
if ok {
print(data)
}
return 0
}
When to use which?
I get asked this a lot: should I use spawn or async? It’s simple:
Use async/await when you need a result returned. It’s the direct, "what happened?" approach.
Use spawn when the task is "fire-and-forget" or when you need complex communication via channels.
A note on the plumbing
If you’re using async/await, the Skink runtime is doing a bit of heavy lifting for you behind the scenes. It links in the necessary concurrency runtime automatically, so you don't have to worry about manual setup. It's built on top of our existing spawn and channel logic, so it plays well with the rest of your system.
Exercises
Don't just take my word for it—get your fingers dirty:
- The Double Collector: Fire off two async tasks concurrently, sum their results, and print the total. It’s a great way to see how much faster things run when they aren't waiting on each other.
- The Error Handler: Write an async task that returns (int, error). Use await to collect the result and handle the error if it pops up.
- The Comparison: Take an old channel-based example from Tutorial 8 and rewrite it using async/await. It’s eye-opening to see how much code you can delete.
Next
When you're ready to start sending raw bits across the network—the true Wild West of programming—we'll talk about Networking with TCP.