What does job.join() do on a Job returned by launch, and how does it differ from awaiting a result?
answer
- join = suspend until done, returns Unit
- join does NOT re-throw the failure to the joiner
- await both waits AND returns value/throws
- joinAll for many jobs
- Structured parent already waits for children
basics
~20 sjoin() is a suspending call that waits until the launched coroutine finishes. It pauses the caller until the job completes but gives back no value — unlike await(), which both waits and returns a result.
solid answer
~50 sjob.join() suspends the calling coroutine until the target Job reaches a completed state (completed normally, cancelled, or failed). It is a regular suspend function, so it never blocks a thread — it just parks the caller. Crucially, join() returns Unit: it gives you no result and, importantly, it does NOT re-throw the launched coroutine's exception to the joiner. A launch failure propagates to the parent and (with a normal Job) cancels siblings via structured concurrency, but the joiner won't see the exception simply by calling join(). This contrasts with Deferred.await() from async, which both suspends until completion and re-throws any failure (or returns the value) into the awaiting code. So join = 'wait for it to be done'; await = 'wait and get the value/exception'. You can join multiple jobs, or use joinAll(jobs) to wait for several.
code
kotlin · 6 linesfun main() = runBlocking {
val a = launch { delay(100); println("A") }
val b = launch { delay(50); println("B") }
joinAll(a, b) // suspends until both finish
println("both done") // prints last
}go deeper
Knows join() waits until the coroutine finishes.
Explains join returns Unit and does not re-throw, contrasts with await, knows joinAll.
Discusses cancellation behavior of join, lazy-job starting, and when structured scope already obviates explicit join.
Reasons about failure-handling design: why join intentionally doesn't surface exceptions, and how that interacts with parent propagation and supervision choices.
## `join()` in one sentence `Job.join()` is a **suspending** function that waits until the coroutine represented by that `Job` has **completed** — meaning it has finished its work, been cancelled, or failed — and then resumes the caller. ```kotlin suspend fun join() ``` Because it is a `suspend` function, it **does not block a thread**; it suspends the calling coroutine and frees the underlying thread to do other work until the joined job finishes. ## What 'completed' covers A `Job` is *completed* when it reaches a terminal state: - **Completed normally** — the block returned. - **Cancelled** — `cancel()` was called and cancellation finished. - **Failed** — the block threw an uncaught exception. `join()` resumes in **all** of these cases. It returns `Unit` regardless. ## The crucial difference from `await()` This is the most common interview trap: - `join()` returns `Unit` and **does not re-throw** the joined coroutine's exception into the joiner. The failure is still handled by **structured concurrency** — it propagates to the parent — but the line `job.join()` itself will not throw the original error to you. - `Deferred.await()` (from `async`) **does** re-throw the failure into the awaiting coroutine and returns the computed value on success. ```kotlin fun main() = runBlocking { val job = launch(start = CoroutineStart.LAZY) { error("boom") } // join resumes after failure but does not throw 'boom' here; // the exception still propagates to the parent scope. job.join() println("after join") } ``` ## Joining many To wait for several launched jobs, collect them and call **`joinAll(vararg jobs)`** (or `jobs.joinAll()`): ```kotlin coroutineScope { val jobs = List(3) { i -> launch { delay(100L * i); println("task $i") } } jobs.joinAll() // suspends until all three finish } ``` Note: inside a `coroutineScope` (or any structured parent), the scope already waits for its children before returning, so explicit `join()` is often unnecessary purely for 'wait until done'. You reach for `join()` when you need to *interleave* waiting at a specific point in your logic. ## Cancellation interaction `join()` is itself cancellable: if the coroutine that called `join()` is cancelled while suspended in it, `join()` throws `CancellationException`. Also, joining a job that you have not started (a `LAZY` job) will **start** it.
- If a launched coroutine fails, does job.join() throw that exception?No. join() resumes when the job completes but returns Unit and does not re-throw it to the joiner. The exception still propagates to the parent via structured concurrency.
- What happens if you join() a job created with CoroutineStart.LAZY?join() starts the lazy job (if not already started) and then suspends until it completes.
join() is waiting for a friend to text 'I'm home' — you know they arrived but get no story; await() is them coming over and telling you exactly what happened (or breaking the news of what went wrong).
saying these in an interview costs you the question
- Claiming join() returns the coroutine's result
- Claiming join() re-throws the launched coroutine's exception to the joiner
- Saying join() blocks the thread instead of suspending
- Confusing join() with await()