Asynchronous Programming
Tree Data Structures
Process Management
Multi-Step Workflows
Software Design Patterns

Clean pattern to manage multi-step async processes on a tree

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Multi-step async workflows over a tree become messy when the structure of the data and the structure of the control flow are mixed together. A clean pattern is to model each tree node as data, define each processing step as a pure async stage, and let one orchestration function walk the tree and combine results. That keeps traversal, business logic, concurrency policy, and error handling separate instead of tangling them in nested promises or callbacks.

Separate Node Data From Workflow Steps

The first step is to make the tree itself boring. It should just describe the hierarchy.

typescript
1type TreeNode = {
2  id: string;
3  children: TreeNode[];
4};

Then define the async steps independently. For example, imagine that every node must be:

  1. fetched from a remote system
  2. validated
  3. stored

Those are workflow stages, not tree-structure concerns.

A Small Pipeline Per Node

A clean way to express node processing is one async function that takes a node and returns a result object.

typescript
1type NodeResult = {
2  id: string;
3  ok: boolean;
4  message: string;
5};
6
7async function processNode(node: TreeNode): Promise<NodeResult> {
8  const payload = await fetchPayload(node.id);
9  await validatePayload(payload);
10  await storePayload(payload);
11
12  return {
13    id: node.id,
14    ok: true,
15    message: "processed",
16  };
17}
18
19async function fetchPayload(id: string) {
20  return { id, value: `payload:${id}` };
21}
22
23async function validatePayload(payload: { id: string; value: string }) {
24  if (!payload.value) {
25    throw new Error(`invalid payload for ${payload.id}`);
26  }
27}
28
29async function storePayload(payload: { id: string; value: string }) {
30  console.log("stored", payload.id);
31}

This isolates the node-level business logic from the traversal strategy.

Orchestrate Tree Traversal Separately

Now build one orchestrator that walks the tree. A depth-first recursive approach is easy to reason about.

typescript
1type TreeResult = {
2  node: NodeResult;
3  children: TreeResult[];
4};
5
6async function processTree(root: TreeNode): Promise<TreeResult> {
7  const node = await processNode(root);
8  const children = [] as TreeResult[];
9
10  for (const child of root.children) {
11    children.push(await processTree(child));
12  }
13
14  return { node, children };
15}

That gives you sequential execution, which is often the simplest and safest default.

Add Controlled Concurrency, Not Unlimited Concurrency

A common mistake is to switch from sequential recursion to Promise.all everywhere and accidentally flood the database or remote API. If sibling nodes can run in parallel, do it deliberately and usually with a limit.

A simple sibling-parallel version looks like this:

typescript
1async function processTreeParallel(root: TreeNode): Promise<TreeResult> {
2  const node = await processNode(root);
3  const children = await Promise.all(root.children.map(processTreeParallel));
4  return { node, children };
5}

That is fine for small trees, but for large ones you usually want a concurrency limiter rather than unbounded fan-out.

Make Error Policy Explicit

Another place tree workflows become ugly is error handling. Decide early whether:

  • one node failure stops the whole tree
  • child failures are collected and processing continues
  • certain errors are retryable while others are terminal

A result wrapper makes that explicit.

typescript
1async function safeProcessNode(node: TreeNode): Promise<NodeResult> {
2  try {
3    return await processNode(node);
4  } catch (error) {
5    return {
6      id: node.id,
7      ok: false,
8      message: error instanceof Error ? error.message : "unknown error",
9    };
10  }
11}

Once failures become data instead of uncaught control flow, aggregation gets much cleaner.

The Pattern In One Sentence

The clean pattern is:

  • data model for the tree
  • pure async pipeline for one node
  • separate traversal orchestrator
  • explicit concurrency policy
  • explicit error policy

That structure scales well because each concern can change independently.

Common Pitfalls

  • Mixing traversal logic and business-step logic in one large recursive function.
  • Using Promise.all blindly and overwhelming external systems or worker capacity.
  • Hiding error-handling policy inside ad hoc try blocks spread across the tree walker.
  • Passing mutable shared state through the whole recursion when result aggregation would be clearer.
  • Treating the tree shape as if it must dictate the workflow stages rather than just the traversal order.

Summary

  • Keep the tree structure as data and the async work as a separate node-level pipeline.
  • Use one orchestrator function to walk the tree and combine results.
  • Start with sequential recursion and add parallelism only when the workload justifies it.
  • Make failure handling a visible policy instead of an accident of nested async code.
  • Clean async tree processing comes from separation of concerns, not from more clever recursion.

Course illustration
Course illustration

All Rights Reserved.