Failed interviews

Receipts of questions I fumbled, kept in public so I never fumble them twice. Each one ships with the answer I should have given.

Sweatworks TypeScript Engineer May 2026

One interview · 2 questions I should have nailed

  1. What are closures?

    Where I slipped: I answered "a function inside a function." That just describes a nested function — basically a callback — not a closure. I even framed it as a function that takes a parameter, which isn't the point at all. Then I really went for it and called array methods like map, forEach and reduce "examples of closures" — they're not. Those are higher-order functions; the callback you hand them is only a closure if it captures variables from the surrounding scope. The part I missed is that a closure is defined by retaining state from its enclosing scope, not by being nested.

    The answer I should've given

    A closure is a function bundled together with references to its surrounding lexical scope. The defining trait isn't the nesting — it's that the inner function keeps access to the outer function's variables even after the outer function has returned, and can read and update that captured state across calls. A nested function (or a callback) only becomes a closure once it actually closes over (remembers) state from its enclosing scope. That retained, private state is the whole point. And array methods like map, forEach or reduce aren't closures either — they're higher-order functions (functions that take a callback); the callback is a closure only when it reaches for variables outside itself.

    function counter() {
      let count = 0;          // captured by the closure
      return () => ++count;   // remembers `count` after counter() returns
    }
    const next = counter();
    next(); // 1
    next(); // 2  ← state persisted via the closure
    ClosuresLexical scopeHigher-order functionsJavaScript
  2. Is JavaScript single-threaded or multi-threaded?

    Where I slipped: I said "single-threaded," then tried to back it up with the event loop and fumbled it. I called the event loop "a queue" and stopped there — true, but I never explained why it's a queue, how it actually works, or how that ties back to there being a single thread. A vague half-answer lands worse than a confident "yes, single-threaded" would have.

    The answer I should've given

    JavaScript runs on a single main thread with one call stack, so it executes one thing at a time. The event loop is what keeps that single thread responsive: it runs the current task to completion, while anything asynchronous — timers, I/O, fetch, DOM events — is handed off to the host (the browser, or Node via libuv), which does that work elsewhere and pushes a callback onto a queue when it's done. There are actually two tiers: the macrotask queue (setTimeout, I/O, UI events) and the microtask queue (Promise callbacks, queueMicrotask). The loop only pulls the next callback once the call stack is empty, and it drains all microtasks before the next macrotask. That ordering — and the fact that one long synchronous block freezes everything — is exactly why it's a queue and not parallel execution. For real parallelism you step outside the language: Web Workers (browser) and Worker Threads (Node) run on separate OS threads and communicate by message passing, with SharedArrayBuffer + Atomics for shared memory. So: single-threaded execution model, concurrency via the event loop, true multithreading via workers.

    console.log('1: sync');                          // runs now, on the stack
    setTimeout(() => console.log('4: macrotask'), 0); // queued as a macrotask
    Promise.resolve().then(() => console.log('3: microtask'));
    console.log('2: sync');
    // → 1, 2, 3, 4
    // stack empties → drain ALL microtasks (3) → then the next macrotask (4)
    Event loopMicrotasksWeb WorkersConcurrency
Overheard Web3 Developer interviews Jun 2026

Not an interview I sat — a question doing the rounds online that I wanted to actually understand.

  1. What is a Merkle tree?

    Where I heard it: This one isn't a question I was asked — I'm not a web3 dev and I never applied. I just kept reading people hiring for web3 roles griping that juniors couldn't explain what a Merkle tree is. So I read an article on it out of curiosity, and the idea of hashing pairs of children over and over to build a tree was too neat to leave off this page.

    What it actually is

    A Merkle tree (or hash tree) is a tree built entirely out of hashes. You split your data into chunks and hash each one — those hashes are the leaves. Then you pair the leaves up, concatenate each pair and hash it to get their parent, and repeat that one level at a time, hashing pairs of children into a single parent, until you're left with one hash at the very top: the Merkle root. That root is a compact fingerprint of the whole dataset — flip a single byte in any leaf and its hash changes, which changes its parent, and that ripples all the way up to the root. The payoff is twofold: it's tamper-evident (any change is visible at the root), and it gives cheap membership proofs. To prove one chunk is in the set you don't need the whole set — just the sibling hash at each level on the path from your leaf up to the root (a "Merkle proof"), which is only about log₂(n) hashes for n leaves. Anyone holding the trusted root can recompute it from your chunk plus those siblings and confirm it matches. That's exactly how a Bitcoin or Ethereum light client checks a transaction is in a block without downloading the whole block, and the same idea underpins Git, IPFS and Certificate Transparency. (Detail worth knowing: when a level has an odd number of nodes, most implementations — Bitcoin included — just duplicate the last hash so it can still be paired.)

    import { createHash } from 'node:crypto';
    const sha = (s) => createHash('sha256').update(s).digest('hex');
    
    // 1. hash each chunk of data → these are the leaves
    let level = ['a', 'b', 'c', 'd'].map(sha);
    
    // 2. hash pairs of children into a parent, repeat up to the top
    while (level.length > 1) {
      const next = [];
      for (let i = 0; i < level.length; i += 2) {
        const left = level[i];
        const right = level[i + 1] ?? left; // odd one out? pair it with itself
        next.push(sha(left + right));
      }
      level = next;
    }
    
    const root = level[0]; // change ANY leaf and this root changes
    Merkle treeHashingBlockchainData structures
micro1 Full-stack Agentic Engineer Sep 2026

An async screen run by micro1's AI recruiter — no human on the other end to read the silence.

  1. Optimistic vs pessimistic locking in Postgres — what is the difference, and when would you reach for each?

    Where I slipped: Blanked. I didn't have either term — I couldn't have told you which one grabs the lock up front and which one lets the write race and checks afterwards. The annoying part is that this isn't an exotic question: it's the plain "two people edit the same row" problem with names attached, and the names are the cheap part.

    The answer I should've given

    Both are answers to the same question: two transactions want to change the same row, and only one of them can be right. Postgres is MVCC, so readers never block writers and writers never block readers — the whole fight is writer versus writer. Pessimistic locking takes the lock up front: SELECT ... FOR UPDATE puts a row-level exclusive lock on the rows it returns, held until the transaction commits or rolls back, and anyone else who tries to lock or update those rows waits at the door. There are weaker flavours (FOR NO KEY UPDATE, FOR SHARE, FOR KEY SHARE) and two escape hatches that actually matter: NOWAIT errors out instead of waiting, and SKIP LOCKED steps over rows someone else already holds — which is how you build a job queue where N workers pull work without ever blocking each other. pg_advisory_xact_lock() does the same job for something that is not a row. The cost is that you are holding a lock: keep a transaction open across user think-time or an HTTP round trip and you get pile-ups, and taking locks in different orders in different code paths gets you deadlocks (Postgres detects those after deadlock_timeout, one second by default, and kills one side). Optimistic locking locks nothing. You read, you compute, and on the way out you assert that nothing moved: UPDATE ... WHERE id = ? AND version = ?, then look at the row count. Zero rows updated means someone committed ahead of you — reload, reapply, retry. A dedicated integer version column is the standard; Postgres also exposes the hidden xmin system column (the transaction id that wrote the current row version), so you can make the same check without adding a column — just know it is a 32-bit counter that reads back as frozen once vacuum freezes an old row, so a real column is safer for long-lived data. There is a third answer worth naming: let the engine do it. REPEATABLE READ and SERIALIZABLE are optimistic at the database level — SERIALIZABLE uses Serializable Snapshot Isolation to track read/write dependencies between concurrent transactions and aborts one with a 40001 serialization failure instead of letting the anomaly through. That is the least application code, but only if you write the retry loop; there is no optimistic concurrency without a retry. And the trap underneath all of it: on the default READ COMMITTED, a plain UPDATE accounts SET balance = balance - 10 WHERE id = 42 is already safe, because the UPDATE takes its own row lock and re-evaluates against the newest version of the row when it unblocks. What is not safe is read-modify-write in application code — SELECT balance, subtract in your language, UPDATE ... SET balance = 90. That is the lost update, and it is the thing both strategies exist to prevent. Choosing: optimistic when conflicts are rare and the transaction spans human time, because nobody should hold a database lock while a user fills in a form; pessimistic when conflicts are the normal case, when a retry is expensive or has side effects you cannot take back, or when the whole point is to hand out a piece of work exactly once.

    -- Pessimistic: take the lock first. Everyone else waits at the door.
    BEGIN;
      SELECT balance FROM accounts WHERE id = 42 FOR UPDATE; -- held until COMMIT
      UPDATE accounts SET balance = balance - 10 WHERE id = 42;
    COMMIT;
    
    -- Optimistic: no lock. Assert on the way out that nobody moved first.
    UPDATE accounts
       SET balance = balance - 10, version = version + 1
     WHERE id = 42 AND version = 7;
    -- 0 rows updated → someone committed ahead of you → reload and retry
    
    -- The one worth memorising: a queue where N workers never block each other.
    SELECT id FROM jobs
     WHERE status = 'pending'
     ORDER BY created_at
     LIMIT 1
       FOR UPDATE SKIP LOCKED;
    PostgresLockingMVCCTransactions