All posts
MermaidFunction CallsCall StackCode DocumentationSoftware Architecture

Mermaid call stack and function flow diagrams for code execution tracing

6 min readThe MermaidCreator team

When code has multiple layers of function calls, conditional branching, and recursive functions, following the execution path in your head becomes difficult. A Mermaid diagram that visualizes function calls makes the flow explicit—ideal for code reviews, onboarding, and debugging documentation. This guide shows how to document call stacks, execution order, and function dependencies with Mermaid.

Why visualize function calls

Code execution order is invisible in source code. A call flow diagram reveals:

  • Execution sequence — which function calls which, and in what order
  • Recursion — how a function calls itself and when it stops
  • Conditional branches — which path is taken based on parameters or state
  • Call depth — how many layers deep execution goes
  • Entry and exit points — where data flows in and out

Without a diagram, a developer reading code must trace through conditionals, loops, and function signatures—a function call diagram does that work upfront.

Basic call flow: sequential functions

Here's the simplest case—a linear chain where A calls B, B calls C:

graph TD
    A["main()"]
    B["parseInput()"]
    C["validateData()"]
    D["processData()"]
    E["returnResult()"]
    
    A -->|calls| B
    B -->|calls| C
    C -->|calls| D
    D -->|calls| E
    E -->|returns| A

The --> arrows show the call direction; |calls| labels clarify the relationship. A developer can trace from main to the bottom without reading code.

Real-world example: user login flow

A login system typically calls several functions. Here's a realistic call stack:

graph TD
    A["login()"]
    B["validateEmail()"]
    C["lookupUser()"]
    D["checkPassword()"]
    E["hashPassword()"]
    F["compareHash()"]
    G["createSession()"]
    H["returnToken()"]
    
    A -->|1. calls| B
    A -->|2. calls| C
    C -->|stores user| DB["Database"]
    A -->|3. calls| D
    D -->|calls| E
    E -->|calls| F
    F -->|success?| G
    G -->|creates| SESSION["Session Store"]
    A -->|4. calls| H
    
    style A fill:#4CAF50,stroke:#2E7D32,color:#fff
    style H fill:#FFC107,stroke:#F57F17,color:#000
    style DB fill:#2196F3,stroke:#1565C0,color:#fff
    style SESSION fill:#2196F3,stroke:#1565C0,color:#fff

The numbers (1, 2, 3, 4) show the call order. Side annotations show dependencies on external systems (Database, Session Store). This reveals:

  • Email validation happens first.
  • Password check is deep (3 functions: validatePassword → hashPassword → compareHash).
  • Session creation is separate from the main chain.

Recursion visualization

Recursive functions can be tricky to understand. Here's a Fibonacci example:

graph TD
    A["fib(5)"]
    B["fib(4)"]
    C["fib(3)"]
    D["fib(3)"]
    E["fib(2)"]
    F["fib(2)"]
    G["fib(1)"]
    H["fib(1)"]
    I["fib(0)"]
    
    A -->|calls| B
    A -->|calls| C
    B -->|calls| D
    B -->|calls| E
    C -->|calls| F
    C -->|calls| G
    D -->|calls| H
    D -->|calls| I

This shows:

  • fib(5) branches into fib(4) and fib(3).
  • fib(3) is called twice (redundant computation—a sign to use memoization).
  • Base cases (fib(1), fib(0)) are leaves.

A developer immediately sees why naive recursion is slow: duplicated calls. This justifies adding a cache.

Conditional call flow

When a function's behavior depends on input, show decision branches:

graph TD
    A["processOrder()"]
    B{Is user<br/>premium?}
    C["applyPremiumDiscount()"]
    D["applyStandardTax()"]
    E["calculateShipping()"]
    F{Express<br/>shipping?}
    G["calculateExpressShipping()"]
    H["calculateStandardShipping()"]
    I["finalizeOrder()"]
    
    A -->|checks| B
    B -->|yes| C
    B -->|no| D
    C --> E
    D --> E
    E -->|checks| F
    F -->|yes| G
    F -->|no| H
    G --> I
    H --> I

The diamond decision nodes show where execution branches. This is useful for code reviews: "Does this function handle both the premium and standard paths correctly?"

Call tree with parameters

For complex functions, show what data flows through calls:

graph TD
    A["getUserProfile(userId=123)"]
    B["fetchUser(123)"]
    C["User {id,email,name}"]
    D["fetchUserPreferences(123)"]
    E["Preferences {theme,lang}"]
    F["fetchUserOrders(123)"]
    G["Orders []"]
    H["buildProfile(User,Prefs,Orders)"]
    I["Profile {user,prefs,orders}"]
    
    A -->|calls with userId| B
    B -->|returns| C
    A -->|calls with userId| D
    D -->|returns| E
    A -->|calls with userId| F
    F -->|returns| G
    A -->|calls with all data| H
    H -->|returns| I

The parameter names and return types make the flow concrete. A reviewer can verify that the right data flows from fetch functions to the builder.

Call stack at a specific moment

When debugging, you want to see the call stack at a breakpoint. Here's a snapshot:

graph TD
    A["main()")
    B["processRequest(req)"]
    C["validateInput(req.body)"]
    D["checkLength(data)"]
    E["🔴 Breakpoint: checkLength returns false"]
    
    A -->|calls| B
    B -->|calls| C
    C -->|calls| D
    D -->|BREAKPOINT| E
    
    style E fill:#FF5252,stroke:#C62828,color:#fff

Stack trace (bottom-to-top): checkLength ← validateInput ← processRequest ← main. A developer can instantly see where execution stopped and trace back to understand the context.

Async/callback flows

For asynchronous code, show the sequence differently:

graph TD
    A["fetchData()"]
    B["makeRequest(url)"]
    C["Request sent →"]
    D["Network delay..."]
    E["Server responds ←"]
    F["onSuccess(data)"]
    G["parseJSON(data)"]
    H["updateUI(parsed)"]
    I["done()"]
    
    A -->|initiates| B
    B -->|async| C
    C -->|waiting| D
    D -->|received| E
    E -->|triggers| F
    F -->|calls| G
    G -->|calls| H
    H -->|callback| I

The async step (C, D, E) shows the non-blocking nature. The callback chain (F → G → H) shows what happens when the response arrives. This helps developers understand "where am I blocked?" vs. "what runs after?"

Best practices for call flow diagrams

  1. Number the calls — use 1., 2., 3. labels to show execution order explicitly.
  2. Show return data — annotate what each function returns (type or variable name).
  3. Keep it focused — diagram one user flow or code path, not the entire system at once.
  4. Use colors by concern — green for success paths, red for errors, blue for external dependencies.
  5. Avoid deep nesting — if a call stack is more than 6–8 levels deep, split into two diagrams (one for the main flow, one for a deep branch).
  6. Test against the code — verify the diagram matches the actual code; stale diagrams mislead.
  7. Add a legend — if you use colors or shapes, explain them at the top.

Comparison table: call flows vs. sequence diagrams

FormatBest forExample
Call flow (graph)Single-threaded, hierarchical executionHow main() calls down through helper functions
Sequence diagramMultiple actors, message passingHow Service A calls Service B, which calls Service C
FlowchartDecision logic and branchingUser login: email → password → session creation

FAQ

How do I show error paths in a call flow diagram?
Use a red diamond for error decisions, or a red "Error" node. Example: validateInput() -->|error| handleError(). Keep error paths separate from the happy path for clarity.

Can I show timeouts and retries?
Yes. Use a loop annotation or a label: makeRequest() -->|timeout, retry| makeRequest(). Mermaid will render the recursive call. Add a comment to explain the retry count.

What if the same function is called from multiple places?
Show all callers pointing to it. This reveals hot spots (functions called frequently). Example: Both loginUser() and signupUser() call hashPassword().

How do I handle mutual recursion (A calls B, B calls A)?
Show the cycle: A -->|calls| B, B -->|calls| A. It's valid, but add a note explaining the termination condition so readers don't assume infinite loops.

Visualize your function calls in the MermaidCreator editor. Clear call stacks and execution flows make code reviews faster and onboarding easier.

Related posts