# How do you trace a bug across multiple files without an IDE?

> Learn a structured method for tracing contracts, data shapes, and state mutations across module boundaries when a bug spans multiple files.

- Canonical HTML: [https://unrust.dev/guides/debugging-across-multiple-files](https://unrust.dev/guides/debugging-across-multiple-files)
- Markdown representation: [https://unrust.dev/guides/debugging-across-multiple-files/index.md](https://unrust.dev/guides/debugging-across-multiple-files/index.md)

Published: 2026-10-22
Published by Unrust. Scoring and editorial method: https://unrust.dev/help#methodology

## Direct answer

To trace a bug across multiple files without an IDE, follow the data contract across each module boundary in order: write down the exact input shape and invariant at the entry caller, verify how each imported helper transforms or normalizes that value, and check whether edge values—such as empty arrays, zero budgets, null fields, or mutated references—mean the same thing on both sides of the boundary.

## Why cross-file bugs survive single-file code review

Many production defects do not look broken inside any single file. The caller file passes a valid-looking options object, the normalization helper applies a sensible-looking default, and the worker module enforces a boundary check—yet the combined pipeline fails because two modules disagree on whether 0, false, or an empty slice means 'disabled' or 'use default'.

When you review each file in isolation, every function appears locally coherent. The defect only becomes visible when you trace one concrete boundary input across the import seam from caller to callee.



## Write down the seam contract at every import boundary

Before jumping back and forth between tabs, pick one failing or boundary input and write down the exact value that crosses each module call.

Check four common cross-file seams: unit or scale mismatches (milliseconds vs seconds, 0-based vs 1-based), sentinel vs falsy normalization (null vs undefined vs false vs 0), shared reference mutation (sorting or splicing an array the caller still reads), and error-contract drift (returning null when the caller expects a thrown error or vice versa).

- [Open the free code review lab](https://unrust.dev/guides/public-code-review-challenges/index.md)
- [Read how to review code against a contract](https://unrust.dev/guides/reading-code-against-a-contract/index.md)


## Pinpoint the module that broke the shared invariant

Once you trace a concrete input across the call chain, identify the first boundary where the actual value diverges from what the downstream consumer assumes.

In Unrust's multi-file workspace drills, you inspect compact 2-to-3-file modules—such as a route handler, a policy helper, and a store—and isolate both the defective file and the exact line where the cross-module contract breaks.



## A cross-file boundary review table

Use this table when a bug spans a caller, a helper or normalizer, and a downstream consumer.

### A configuration flag or numeric limit passes through a normalization module before reaching the worker

- **Review decision:** Trace explicit falsy values (false, 0, '') across the normalizer into the worker
- **Evidence to verify:** Verify whether truthiness fallback (||) or loose checks overwrite a valid caller-supplied boundary value

### Two modules share an array, map, or options object across synchronous or async calls

- **Review decision:** Check whether the callee mutates the passed reference in place (.sort(), .splice(), delete, key assignment)
- **Evidence to verify:** Confirm whether the caller reads the same reference after the call expecting its original order or contents

### A helper catches or transforms errors before returning to the orchestrator

- **Review decision:** Compare the helper's failure return value (null, -1, empty array, partial result) with the caller's branch condition
- **Evidence to verify:** Verify that a failed or empty lookup is not treated as a valid success state by the caller

## Try the trace: where does the cross-file contract break?

In config.ts, normalizeOptions(raw) returns { retries: raw.retries || 3 }. In client.ts, sendWithRetry(opts) loops while attempt <= opts.retries. A caller in healthcheck.ts calls sendWithRetry(normalizeOptions({ retries: 0 })) expecting a single probe with zero retries. Why does the probe still retry 3 times?

- The loop in client.ts ignores opts.retries when it is a positive integer.
- normalizeOptions in config.ts uses || 3, which treats the caller's explicit 0 as falsy and replaces it with 3 before client.ts ever sees it.
- healthcheck.ts forgot to pass an object to normalizeOptions.
- TypeScript converts numeric literal 0 to undefined at compile time.

**Reasoning:** Cross-file bugs become straightforward when you carry one concrete boundary value ({ retries: 0 }) across each module call instead of reading each file with generic placeholder assumptions.

## Practice loop

1. Pick one concrete edge input at the entry file—such as 0, false, null, or a two-element collection—before reading imported files.
2. Write down the exact value returned or mutated at each module boundary as that input moves from caller to helper to consumer.
3. Mark the first file and line where the value crossing the boundary violates the downstream function's contract.

## Common questions

### Why practice multi-file debugging on small 2-to-3-file workspaces instead of a 50-file repo?

Most cross-module bugs fail at a single seam between two or three collaborating functions. Practicing on compact multi-file workspaces removes boilerplate noise so you can train the habit of verifying data contracts across imports.

### Does Unrust include multi-file drills?

Yes. Unrust's practice library includes cross-file workspace drills where you inspect collaborating files and pinpoint the exact module and line that breaks the shared contract.

## Limitations

Manual cross-file tracing complements—and does not replace—static type checking, integration tests, and distributed tracing in large codebases. Unrust's diagnostic and multi-file workspace drills use focused module sets to train boundary reasoning and do not audit your production repository.

## Provenance

Original editorial guide by the Unrust team based on interface-contract analysis and multi-file fault-isolation patterns.

## Sources

- [Interface Misuse and Cross-Module Defect Patterns in Software Systems](https://ieeexplore.ieee.org/xpl/RecentIssue.jsp?punumber=32) — IEEE Transactions on Software Engineering. Studies how mismatched assumptions about boundary values, ownership, and error signaling across module interfaces produce defects.
- [Strategic Debugging: Finding Where the Bug Is](https://ieeexplore.ieee.org/document/6205756) — IEEE Software. Outlines how tracing data flow across call boundaries narrows the fault region systematically.

## Related guides

- [How do you read code against its contract?](https://unrust.dev/guides/reading-code-against-a-contract/index.md): Learn how to compare implementation with intent, find mismatched assumptions, and catch bugs that clean-looking code can hide.
- [How do you debug code without AI?](https://unrust.dev/guides/debugging-without-ai/index.md): A practical debugging loop for developers who want to reason through a failure before asking an AI assistant or search engine for a fix.
- [Why does a TypeScript default erase an explicit false?](https://unrust.dev/guides/typescript-explicit-false-challenge/index.md): Test a boolean default with false and an omitted field, then use nullish coalescing to preserve an explicit user choice.

Next step: [Take the free diagnostic](https://unrust.dev/diagnostic/index.md)
