Day 26: Event Loop & Concurrency


 Day 26: Event Loop & Concurrency


1. Learning Objectives

By the end of this lesson, you will be able to:

  • Explain JavaScript's single-threaded, non-blocking execution model
  • Describe the components of the event loop: call stack, heap, callback queue, microtask queue
  • Distinguish between macrotasks (setTimeout, setInterval, I/O) and microtasks (Promise callbacks, queueMicrotask, MutationObserver)
  • Predict the execution order of mixed synchronous, microtask, and macrotask code
  • Understand how async/await and Promises interact with the event loop
  • Recognize and avoid common pitfalls like blocking the event loop and starving the microtask queue
  • Use requestAnimationFrame for smooth visual updates
  • Apply concurrency patterns like cooperative yielding and task splitting

2. Theory

JavaScript is Single-Threaded

JavaScript has a single thread with one call stack. It can do only one thing at a time. However, the browser (or Node.js) provides Web APIs (like setTimeout, fetch, DOM events) that run outside the main thread. The event loop coordinates when their callbacks are executed.

The Call Stack

The call stack is a LIFO (Last In, First Out) data structure that tracks function calls. When a function is called, a frame is pushed onto the stack. When it returns, the frame is popped.

function multiply(a, b) { return a * b; }
function square(n) { return multiply(n, n); }
function printSquare(n) { console.log(square(n)); }

printSquare(5); // Stack: printSquare → square → multiply → (returns) → square → printSquare

If the stack grows too large (e.g., infinite recursion), we get a Stack Overflow error.

The Event Loop Components


┌───────────────────────────────────────────┐
│              Call Stack                   │
│  (synchronous execution)                  │
└────────────────┬──────────────────────────┘
                 │
                 ▼
┌───────────────────────────────────────────┐
│         Microtask Queue                   │
│  (Promise callbacks, queueMicrotask,      │
│   MutationObserver)                       │
│  Processed until empty before next        │
│  macrotask                                │
└────────────────┬──────────────────────────┘
                 │
                 ▼
┌───────────────────────────────────────────┐
│         Macrotask Queue                   │
│  (setTimeout, setInterval, I/O,           │
│   UI events, setImmediate)                │
│  One task per event loop tick             │
└───────────────────────────────────────────┘
                 │
                 ▼
         Render (if needed)
         requestAnimationFrame callbacks

Event loop algorithm (simplified):

  1. Execute all synchronous code on the call stack.
  2. Process all microtasks in the microtask queue (until empty).
  3. Take one macrotask from the macrotask queue and execute it.
  4. If rendering is needed, run requestAnimationFrame callbacks and repaint.
  5. Repeat from step 2.

Macrotasks vs Microtasks

TypeExamplesPriority
SynchronousRegular function calls, console.log, arithmeticExecutes immediately on the call stack
MicrotasksPromise.then/catch/finally, queueMicrotask, MutationObserver, process.nextTick (Node.js)Higher priority — processed before the next macrotask
MacrotaskssetTimeout, setInterval, setImmediate (Node.js), I/O callbacks, UI events, postMessageLower priority — one per event loop tick
console.log("1");                    // Sync

setTimeout(() => console.log("2"), 0); // Macrotask

Promise.resolve().then(() => console.log("3")); // Microtask

console.log("4");                    // Sync

// Output: 1, 4, 3, 2

Explanation:

  1. 1 and 4 are synchronous — they run immediately.
  2. The Promise .then() is a microtask — it runs after all sync code but before the next macrotask.
  3. setTimeout (even with 0ms) is a macrotask — it runs after microtasks are drained.

Starving the Microtask Queue

If microtasks continuously add more microtasks, the macrotask queue never gets processed. This can freeze the UI.

function recursiveMicrotask() {
    queueMicrotask(() => {
        console.log("microtask");
        recursiveMicrotask(); // Adds another microtask — infinite loop!
    });
}
recursiveMicrotask();
// This will never stop, and setTimeout callbacks never run.

Solution: Use a macrotask (e.g., setTimeout) to break the chain, allowing other tasks to execute.


requestAnimationFrame

requestAnimationFrame schedules a callback before the next repaint. It's ideal for animations and visual updates.

function animate(timestamp) {
    element.style.left = `${(timestamp / 10) % 400}px`;
    requestAnimationFrame(animate);
}
requestAnimationFrame(animate);

requestAnimationFrame callbacks run after microtasks but before the browser repaints. They are not strictly microtasks or macrotasks — they run in the render phase.


Blocking the Event Loop

If a synchronous operation takes too long (e.g., a heavy loop, complex computation, or JSON.parse on a huge object), the entire page freezes because no other code can run.

// BAD: blocks the event loop for 3 seconds
const start = Date.now();
while (Date.now() - start < 3000) {
    // busy wait
}
console.log("Done");

// GOOD: yield to the event loop using async/await or setTimeout
function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}
async function doWork() {
    for (let i = 0; i < 10; i++) {
        await sleep(300); // yield control
        console.log(`Step ${i}`);
    }
}

3. Code Examples

Example 1: Basic Event Loop Order

console.log("=== Event Loop Order ===\n");

console.log("1: Sync start");

setTimeout(() => console.log("2: setTimeout (macrotask)"), 0);

Promise.resolve().then(() => console.log("3: Promise.then (microtask)"));

queueMicrotask(() => console.log("4: queueMicrotask (microtask)"));

setTimeout(() => console.log("5: setTimeout 2 (macrotask)"), 0);

Promise.resolve().then(() => {
    console.log("6: Promise.then inner (microtask)");
    queueMicrotask(() => console.log("7: microtask inside microtask"));
});

console.log("8: Sync end");

// Output:
// 1: Sync start
// 8: Sync end
// 3: Promise.then (microtask)
// 4: queueMicrotask (microtask)
// 6: Promise.then inner (microtask)
// 7: microtask inside microtask
// 2: setTimeout (macrotask)
// 5: setTimeout 2 (macrotask)

Example 2: Microtask Queue Depth

console.log("=== Microtask Depth ===\n");

console.log("Start");

Promise.resolve()
    .then(() => {
        console.log("Microtask 1");
        return Promise.resolve();
    })
    .then(() => {
        console.log("Microtask 2");
        return Promise.resolve();
    })
    .then(() => {
        console.log("Microtask 3");
    });

console.log("End");

// Output:
// Start
// End
// Microtask 1
// Microtask 2
// Microtask 3

// All microtasks run before any macrotask, even if they chain.

Example 3: setTimeout(0) vs Promise vs process.nextTick (Node.js)

// Run in Node.js
console.log("Start");

setTimeout(() => console.log("setTimeout"), 0);

setImmediate(() => console.log("setImmediate"));

process.nextTick(() => console.log("nextTick"));

Promise.resolve().then(() => console.log("Promise"));

console.log("End");

// Output:
// Start
// End
// nextTick
// Promise
// setTimeout
// setImmediate

Order in Node.js:

  1. Sync code
  2. process.nextTick (highest priority microtask)
  3. Other microtasks (Promise callbacks)
  4. Macrotasks: setTimeout then setImmediate (order may vary depending on phase)

Example 4: Blocking the Event Loop and How to Fix It

// ---- Blocking the Event Loop ----
console.log("=== Blocking Demo ===\n");

function blockFor(ms) {
    const start = Date.now();
    while (Date.now() - start < ms) {
        // Busy wait — blocks everything
    }
}

console.log("Before block");
blockFor(2000); // Blocks for 2 seconds — UI freezes, no events processed
console.log("After block");

// ---- Non-blocking Approach ----
function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

async function chunkedWork() {
    console.log("Starting chunked work...");
    for (let i = 0; i < 5; i++) {
        // Simulate a small unit of work
        const result = i * i;
        console.log(`Chunk ${i}: ${result}`);

        // Yield to the event loop — allows other tasks to run
        await sleep(0); // Schedule next chunk as a macrotask
    }
    console.log("Chunked work complete.");
}

chunkedWork();
console.log("This runs immediately — not blocked!");

Example 5: requestAnimationFrame and Rendering

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Animation Frame Demo</title>
    <style>
        #box {
            width: 50px;
            height: 50px;
            background: red;
            position: relative;
        }
        #log { font-family: monospace; font-size: 12px; margin-top: 20px; }
    </style>
</head>
<body>
    <div id="box"></div>
    <div id="log"></div>
    <script>
        const box = document.getElementById("box");
        const log = document.getElementById("log");
        let startTime = null;
        let frameCount = 0;

        function step(timestamp) {
            if (!startTime) startTime = timestamp;
            const elapsed = timestamp - startTime;

            // Move the box in a circle
            const x = 200 + 150 * Math.cos(elapsed / 500);
            const y = 100 + 150 * Math.sin(elapsed / 500);
            box.style.left = `${x}px`;
            box.style.top = `${y}px`;

            frameCount++;
            if (elapsed > 3000) {
                log.textContent = `Animation ran for ${(elapsed / 1000).toFixed(1)}s, ${frameCount} frames`;
                return; // stop after 3 seconds
            }
            requestAnimationFrame(step);
        }

        requestAnimationFrame(step);

        // Demonstrate that requestAnimationFrame runs before repaint
        let microtaskCount = 0;
        function addMicrotask() {
            queueMicrotask(() => {
                microtaskCount++;
                if (microtaskCount < 5) addMicrotask();
            });
        }
        addMicrotask();
        console.log("Microtasks queued — they run before rAF and repaint.");
    </script>
</body>
</html>

Example 6: Cooperative Yielding for Heavy Computations

// ---- Splitting a Heavy Task ----

// Simulate a heavy computation (e.g., processing 10 million items)
function heavyTask(items) {
    let result = 0;
    for (let i = 0; i < items; i++) {
        result += Math.sqrt(i);
    }
    return result;
}

// BAD: blocks the event loop
console.time("Heavy sync");
console.log(heavyTask(10000000));
console.timeEnd("Heavy sync");
// UI freezes for a noticeable time

// GOOD: chunk the work and yield
async function heavyTaskAsync(items, chunkSize = 100000) {
    let result = 0;
    let i = 0;

    function processChunk() {
        const end = Math.min(i + chunkSize, items);
        for (; i < end; i++) {
            result += Math.sqrt(i);
        }
        if (i < items) {
            // Yield to the event loop before the next chunk
            return new Promise(resolve => {
                setTimeout(() => {
                    resolve(processChunk());
                }, 0);
            });
        }
        return result;
    }

    return processChunk();
}

async function runHeavyAsync() {
    console.time("Heavy async");
    const result = await heavyTaskAsync(10000000);
    console.log(result);
    console.timeEnd("Heavy async");
    console.log("UI remained responsive during computation!");
}

runHeavyAsync();

4. Exercises

Beginner

  1. Predict and verify the output of:

    console.log("A");
    setTimeout(() => console.log("B"), 0);
    console.log("C");
    
  2. Write code that logs "1" synchronously, then uses a microtask to log "2", and a macrotask to log "3".

  3. What is the output of:

    Promise.resolve().then(() => console.log("then"));
    console.log("sync");
    

Intermediate

  1. Write a function measureFrameRate() that uses requestAnimationFrame to count how many frames occur in 2 seconds.

  2. Create a function yieldingMap(arr, fn, chunkSize) that processes an array in chunks, yielding to the event loop between chunks, and returns a Promise that resolves with the mapped array.

  3. Fix this code that potentially starves the microtask queue:

    function processItems(items) {
        items.forEach((item, index) => {
            queueMicrotask(() => {
                console.log(index, item);
                processItems(items.slice(1)); // recursive!
            });
        });
    }
    

Advanced

  1. Implement a cooperative scheduler scheduler(tasks, concurrency) that runs multiple async tasks with a limit on how many execute per event loop tick, yielding between ticks.
  2. Build a progress bar that updates visually during a long computation by yielding to the event loop with requestAnimationFrame or setTimeout(0). Show actual percentage completion.

5. Mini Project: Event Loop Visualizer

Build an interactive visualizer that demonstrates the event loop's behavior with different task types.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Event Loop Visualizer</title>
    <style>
        * { box-sizing: border-box; margin: 0; padding: 0; }
        body {
            font-family: monospace;
            background: #1e1e1e;
            color: #d4d4d4;
            max-width: 700px;
            margin: 30px auto;
            padding: 20px;
        }
        h1 { text-align: center; margin-bottom: 20px; color: #569cd6; }
        .controls { display: flex; gap: 10px; flex-wrap: wrap; justify-content: center; margin-bottom: 20px; }
        .controls button {
            padding: 10px 16px; border: none; border-radius: 4px;
            cursor: pointer; font-family: monospace; font-size: 14px;
            transition: transform 0.1s;
        }
        .controls button:hover { transform: scale(1.05); }
        .controls button:active { transform: scale(0.95); }
        .btn-sync { background: #4ec9b0; color: #000; }
        .btn-micro { background: #c586c0; color: #000; }
        .btn-macro { background: #dcdcaa; color: #000; }
        .btn-raf { background: #ce9178; color: #000; }
        .btn-clear { background: #f44747; color: #fff; }
        .btn-block { background: #e06c75; color: #000; }
        #output {
            background: #252526;
            border: 1px solid #3c3c3c;
            border-radius: 4px;
            padding: 15px;
            min-height: 300px;
            max-height: 500px;
            overflow-y: auto;
            font-size: 14px;
            line-height: 1.6;
        }
        #output .log-entry { display: flex; gap: 10px; align-items: center; }
        #output .time { color: #6a9955; font-size: 12px; }
        #output .tag { padding: 2px 6px; border-radius: 3px; font-size: 11px; font-weight: bold; }
        .tag-sync { background: #4ec9b0; color: #000; }
        .tag-micro { background: #c586c0; color: #000; }
        .tag-macro { background: #dcdcaa; color: #000; }
        .tag-raf { background: #ce9178; color: #000; }
        .tag-block { background: #e06c75; color: #000; }
        .info { text-align: center; margin-top: 10px; color: #888; font-size: 13px; }
    </style>
</head>
<body>
    <h1>🔄 Event Loop Visualizer</h1>

    <div class="controls">
        <button class="btn-sync" id="btn-sync">➕ Sync</button>
        <button class="btn-micro" id="btn-micro">➕ Microtask</button>
        <button class="btn-macro" id="btn-macro">➕ Macrotask</button>
        <button class="btn-raf" id="btn-raf">➕ rAF</button>
        <button class="btn-block" id="btn-block">⚠️ Block (2s)</button>
        <button class="btn-clear" id="btn-clear">🗑️ Clear</button>
    </div>

    <div id="output">
        <div class="log-entry">
            <span class="time">—</span>
            <span>Click a button to see the event loop in action</span>
        </div>
    </div>
    <div class="info">Check the console for additional timing details</div>

    <script>
        const output = document.getElementById("output");
        let entryCount = 0;

        function addLog(message, tagClass, tagText) {
            entryCount++;
            const now = new Date();
            const time = now.toLocaleTimeString("en-US", { hour12: false }) + "." + String(now.getMilliseconds()).padStart(3, "0");
            const div = document.createElement("div");
            div.className = "log-entry";
            div.innerHTML = `
                <span class="time">${time}</span>
                <span class="tag ${tagClass}">${tagText}</span>
                <span>${message}</span>
            `;
            output.appendChild(div);
            output.scrollTop = output.scrollHeight;
        }

        // Synchronous
        document.getElementById("btn-sync").addEventListener("click", () => {
            addLog("Synchronous code executed", "tag-sync", "SYNC");
        });

        // Microtask (Promise)
        document.getElementById("btn-micro").addEventListener("click", () => {
            addLog("Microtask queued (Promise)", "tag-micro", "QUEUE");
            Promise.resolve().then(() => {
                addLog("Microtask executed (Promise.then)", "tag-micro", "MICRO");
            });
        });

        // Microtask (queueMicrotask)
        document.getElementById("btn-micro").addEventListener("dblclick", () => {
            addLog("Microtask queued (queueMicrotask)", "tag-micro", "QUEUE");
            queueMicrotask(() => {
                addLog("Microtask executed (queueMicrotask)", "tag-micro", "MICRO");
            });
        });

        // Macrotask (setTimeout)
        document.getElementById("btn-macro").addEventListener("click", () => {
            addLog("Macrotask queued (setTimeout 0)", "tag-macro", "QUEUE");
            setTimeout(() => {
                addLog("Macrotask executed (setTimeout)", "tag-macro", "MACRO");
            }, 0);
        });

        // requestAnimationFrame
        document.getElementById("btn-raf").addEventListener("click", () => {
            addLog("rAF queued", "tag-raf", "QUEUE");
            requestAnimationFrame(() => {
                addLog("rAF callback executed", "tag-raf", "rAF");
            });
        });

        // Blocking
        document.getElementById("btn-block").addEventListener("click", () => {
            addLog("Blocking for 2 seconds...", "tag-block", "BLOCK");
            console.time("Block");
            const start = Date.now();
            while (Date.now() - start < 2000) {
                // busy wait
            }
            console.timeEnd("Block");
            addLog("Block finished!", "tag-block", "BLOCK");
        });

        // Clear
        document.getElementById("btn-clear").addEventListener("click", () => {
            output.innerHTML = '<div class="log-entry"><span class="time">—</span><span>Cleared. Click a button to start.</span></div>';
            entryCount = 0;
        });

        // Demo: queue multiple at once to see the order
        document.addEventListener("dblclick", () => {
            addLog("=== Running demo: sync → micro → macro ===", "tag-sync", "DEMO");
            console.log("Sync 1");
            setTimeout(() => {
                addLog("Macro: setTimeout(0) ran", "tag-macro", "MACRO");
                console.log("Macro");
            }, 0);
            Promise.resolve().then(() => {
                addLog("Micro: Promise.then ran", "tag-micro", "MICRO");
                console.log("Micro");
            });
            console.log("Sync 2");
            addLog("Demo done (check console for order: sync, sync, micro, macro)", "tag-sync", "DEMO");
        });

        console.log("Event Loop Visualizer ready. Click buttons or double-click for demo.");
    </script>
</body>
</html>

6. Common Mistakes

MistakeWhy it's wrongCorrect approach
Blocking the event loop with long synchronous operationsFreezes the UI and prevents all other tasks from executingYield with setTimeout(0), queueMicrotask, or requestAnimationFrame
Assuming setTimeout(fn, 0) runs immediatelyExecutes only after all sync code and microtasks are doneUnderstand that 0ms means "as soon as possible" but not immediately
Starving the microtask queueIf microtasks keep adding more microtasks, macrotasks never runLimit microtask recursion or use macrotasks to break the chain
Using for...of with await and expecting parallelismIterates sequentially, not in parallelUse Promise.all() for concurrent operations
Expecting requestAnimationFrame to run at a fixed intervalRuns when the browser is ready to repaint (typically 60fps)Use setInterval for fixed intervals; use rAF for smooth animations
Not understanding that Promise.resolve().then() is a microtaskthen() callbacks run before the next macrotask, not immediatelyUse this knowledge to control execution order
Creating deep Promise chains that starve renderingAll microtasks run before rAF and repaint, delaying visual updatesInsert setTimeout(0) or requestAnimationFrame to allow rendering

7. Best Practices

  • Avoid blocking the main thread — break large synchronous tasks into chunks and yield between them.
  • Use requestAnimationFrame for visual updates — it synchronizes with the browser's repaint cycle.
  • Use Promise.all() for concurrent async operations rather than sequential await.
  • Use queueMicrotask() sparingly — it can starve other tasks if overused.
  • Prefer setTimeout(0) to yield control — it's a macrotask, so it allows microtasks and rendering to run.
  • Understand the execution order — sync → microtasks → requestAnimationFrame → render → macrotasks.
  • Use Web Workers for CPU-intensive tasks — they run in a separate thread and don't block the UI.
  • Limit Promise chain depth — deep chains can delay rendering.
  • Test with realistic workloads — a simple demo might not reveal event loop issues.
  • Use the browser's Performance tab to visualize event loop activity and frame drops.

8. Challenge Assignment (Optional)

"Cooperative Task Scheduler" Challenge

Build a task scheduler that runs multiple heavy computations while keeping the UI responsive.

Requirements:

  1. Create a Scheduler class that:

    • addTask(name, fn, priority) — adds a task function (returns a Promise)
    • start() — begins processing tasks in the event loop's idle time
    • stop() — pauses processing
    • getQueueLength() — returns pending task count
    • on("taskComplete", callback) — event when a task finishes
    • on("drain", callback) — event when all tasks are done
  2. Each task should be broken into chunks that yield to the event loop using setTimeout(0) or requestIdleCallback.

  3. Support priority levels: high, normal, low. High-priority tasks run first.

  4. Show a progress indicator in the DOM that updates as tasks complete.

Bonus:

  • Add preemptive priority — a high-priority task can interrupt a running low-priority task
  • Create a visual dashboard that shows task queue, currently running task, and completed tasks
  • Implement task cancellation via an AbortController

Example usage:

const scheduler = new Scheduler();
scheduler.addTask("Calculate primes", () => calculatePrimes(100000), "low");
scheduler.addTask("Process data", () => processLargeArray(data), "high");
scheduler.addTask("Generate report", () => generateReport(), "normal");

scheduler.on("taskComplete", (name) => console.log(`✅ ${name} done`));
scheduler.on("drain", () => console.log("All tasks complete"));

scheduler.start();

9. Knowledge Check (Quiz)

  1. What is the correct order of execution in the event loop?

    • a) Macrotasks → Microtasks → Sync → Render
    • b) Sync → Microtasks → Render → Macrotasks
    • c) Sync → Macrotasks → Microtasks → Render
    • d) Microtasks → Sync → Macrotasks → Render
  2. What will this code output?

    console.log("A");
    setTimeout(() => console.log("B"), 0);
    Promise.resolve().then(() => console.log("C"));
    console.log("D");
    
    • a) A, B, C, D
    • b) A, D, C, B
    • c) A, C, D, B
    • d) A, D, B, C
  3. Which queue has higher priority: microtasks or macrotasks?

    • a) Microtasks
    • b) Macrotasks
    • c) They have equal priority
    • d) It depends on the browser
  4. What is the main problem with this code?

    function process(items) {
        items.forEach((item, i) => {
            queueMicrotask(() => {
                console.log(i);
                if (i < items.length - 1) process(items.slice(i + 1));
            });
        });
    }
    
    • a) It's recursive and causes a stack overflow
    • b) It starves the microtask queue, potentially blocking macrotasks
    • c) It's not async
    • d) queueMicrotask doesn't exist
  5. When does requestAnimationFrame callback execute?

    • a) Immediately after being queued
    • b) After all microtasks but before the next paint
    • c) In the next macrotask
    • d) At a random interval
  6. How can you yield control to allow other tasks to run during a long computation?

    • a) Use a while loop with a break condition
    • b) Use setTimeout(0) or queueMicrotask to schedule the next chunk
    • c) Use Promise.resolve().then() recursively
    • d) Both b and c can work, but b is safer
  7. What does process.nextTick() do in Node.js?

    • a) Schedules a macrotask
    • b) Schedules a microtask with higher priority than Promise callbacks
    • c) Runs immediately, blocking the event loop
    • d) Schedules a callback in the next event loop iteration
  8. How can you run CPU-intensive work without blocking the UI?

    • a) Use setTimeout to split work into small chunks
    • b) Use a Web Worker
    • c) Use requestIdleCallback
    • d) All of the above

Answers: 1-b, 2-b (A sync, D sync, C microtask, B macrotask), 3-a, 4-b (microtask starvation), 5-b, 6-d, 7-b, 8-d


10. Additional Resources


11. Summary

Today you learned:

  • JavaScript is single-threaded but uses an event loop to handle asynchronous operations
  • The call stack executes synchronous code; long operations block it
  • Microtasks (Promise callbacks, queueMicrotask) have higher priority than macrotasks (setTimeout, setInterval, I/O)
  • requestAnimationFrame runs before the browser repaints, ideal for animations
  • Blocking the event loop freezes the UI; use chunking and yielding to avoid it
  • Starving the microtask queue prevents macrotasks from running; limit microtask recursion
  • Cooperative yielding with setTimeout(0) or queueMicrotask keeps the UI responsive
  • Web Workers provide true parallelism for CPU-intensive tasks

You now have a deep understanding of how JavaScript manages concurrency — essential knowledge for building responsive, performant applications. Tomorrow we'll explore performance and memory optimization.


"The event loop is the heartbeat of JavaScript. Understanding its rhythm is the key to writing code that is both responsive and efficient."

Comments

EARN AT THE COMFORT OF YOUR HOME

Sponsored content

Popular posts from this blog

Day 5 – Functions, Scope, and Docstrings

Day 10 – Exception Handling and Debugging

Day 1 – Python Foundations for AEC Professionals