Debugging & troubleshooting
Find out why fallow reports a finding, or why it misses one. Trace exports, files, dependencies, and clones, profile slow runs, and fix common false positives.
When fallow reports something you did not expect, or misses something you did expect, this page shows you how to find the cause. Trace flags show how fallow reached a result, and --performance shows where the run spends its time.
Tracing export usage
To see the full usage chain for one export, pass --trace FILE:EXPORT.
fallow dead-code --trace src/utils/format.ts:formatCurrency
USED formatCurrency in src/utils/format.ts
File: reachable
Namespace: value
Reason: Used by 2 file(s), re-exported through 1 barrel(s)
2 direct reference(s):
-> src/components/Price.ts (named import)
-> src/pages/Checkout.ts (named import)
Re-exported through:
-> src/utils/index.ts as 'formatCurrency' (1 ref(s))
Fallow prints every file that imports formatCurrency, and every barrel that re-exports it. For an unused export, the status is UNUSED and the Reason line tells you why. Use it when:
- Fallow reports an export as unused, and you expected it to be used.
- You want to know which files depend on a symbol before you remove it.
- Fallow does not resolve a re-export chain the way you expect.
Tracing file edges
To see all incoming and outgoing module graph edges for one file, pass --trace-file PATH.
fallow dead-code --trace-file src/components/Button.tsx
Fallow shows every import in the file and every file that imports from it. When a Module Federation exposes entry makes the file an entry point, the trace names each config that exposes it. See Trace the Federation source.
When a file exists at the exact path from the project root, dead-code --trace and --trace-file take that file. In a monorepo, src/a.ts then never selects packages/x/src/a.ts. A short path that matches more than one file takes the first match, so pass the full project-relative path.
Use --trace-file when:
- Fallow reports a file as unused, and you think something references it.
- You want to check that an entry point reaches the file.
- Fallow marks all exports of a file as unused. The cause can be that no entry point reaches the file.
Tracing dependency usage
To find every place that uses a package, pass --trace-dependency PACKAGE. This covers imports and script binaries.
fallow dead-code --trace-dependency moment
UNUSED moment (0 import(s))
Fallow lists each file that imports the package and marks type-only imports. When a package.json script or a CI config uses a binary of the package, fallow says so. When the name is a Module Federation remote alias, the trace names each config that declares it, and says that a remote container provides the name. Use it when:
- Fallow reports a dependency as unused, and you think a script or a config file uses it.
- You want to know which parts of your codebase depend on a package.
- You want to know if you can remove a dependency.
Tracing duplication clones
To see all clone instances for one source location, pass dupes --trace FILE:LINE.
fallow dupes --trace src/utils/validate.ts:42
Fallow shows every other location in the same clone group as the code at that line. Use it when:
- You want to find all copies of a duplicated block before you refactor.
- You must decide whether to extract a shared function or module.
- A duplication finding looks wrong, and you want to see the matched instances.
Performance profiling
To see how long each pipeline stage takes, pass --performance.
fallow dead-code --performance
fallow dupes --performance
┌─ Pipeline Performance ─────────────────────────────
│ discover files: 13.2ms (520 files)
│ workspaces: 2.1ms (0 workspaces)
│ plugin detection: 21.4ms (+10.5ms plugin globs under entry points)
│ script analysis: 3.0ms
│ parse/extract: 17.6ms (520 modules, 520 cached, 0 parsed)
│ cache update: 2.0ms
│ entry points: 12.3ms (92 entries)
│ root package 1.5ms
│ workspaces 0.0ms
│ plugin globs 10.5ms
│ compile 9.9ms
│ match 0.2ms
│ (other) 0.4ms
│ infrastructure 0.2ms
│ dynamic globs 0.0ms
│ dedup 0.0ms
│ (other) 0.0ms
│ resolve imports: 0.0ms
│ build graph: 5.1ms
│ analyze: 19.3ms
│ (other): 0.0ms
│ ────────────────────────────────────────────────
│ TOTAL: 61.4ms
│ rows are per-stage costs; several run outside or beside the TOTAL clock
├─ Process ──────────────────────────────────────────
│ startup: 1.3ms (thread pool 0.1ms)
│ config: 1.1ms
│ analysis: 97.0ms (the stage rows above)
│ after analysis: 0.2ms
│ output: 12.9ms
│ (other): 0.4ms
│ ────────────────────────────────────────────────
│ WALL: 112.9ms
│ work: 0 files read, 0 source bytes, 2481337 parse cache bytes
│ resolve: 0 specifier calls (0 unique), 0 resolver calls, 0 canonicalize calls
└───────────────────────────────────────────────────
The indented rows under entry points split that stage into parts. They are not added to the TOTAL sum. Fallow shows them only when the stage takes 5ms or more, so the box stays short for a fast project. When entry-point discovery is the slow stage, these rows show if the cost is plugin glob compilation, workspace traversal, or something else.
Read the rows as the cost of each stage, not as parts of TOTAL. TOTAL is the wall clock of the dead-code analysis, from workspace and plugin loading until the detectors finish. File discovery, parsing and the cache update run before that clock starts, so on a warm run the rows can add up to more than TOTAL. The (other) row is TOTAL minus the stage rows, with a minimum of 0.0ms.
In combined mode (fallow with no subcommand), the duplication stage can run at the same time as the rest of the pipeline. Fallow then marks it (concurrent). When the two passes share one file walk, duplication runs after the dead-code pass, and fallow marks it (after dead code). Fallow does not add it to TOTAL in either case. When duplication detection takes longer than the dead-code pass, this stage can be larger than TOTAL.
Process time
The Process section covers the time outside the pipeline stages. The rows are parts of one clock, so they add up: the rows plus (other) give WALL, the time from process start to the table.
startupis argument parsing and setup before the command runs. The thread pool is part of it.configis the config load for the command.gitappears when--changed-sinceruns git.analysisholds all the stage rows above it.after analysisis the scope filters, rules, baselines and gates.outputis the report serialization and write.
dead-code prints the table after the report, so the output row is complete. In combined mode, the table prints before the health analysis and the report, so WALL covers only the time until then.
Work counts
The work and resolve rows are exact counts, not times. They do not change from run to run on the same project and commit, so you can compare two runs with exact equality:
- files and source bytes that the parse stage read from disk,
- bytes of the parse cache that the run read,
- specifier resolutions that the imports asked for, and the distinct specifiers per file,
- calls into the module resolver and path canonicalize calls.
A resolver count of zero means that the run reused the module graph cache. More specifier calls than distinct specifiers means that some files import the same specifier more than once. The health timings show git_log_bytes next to the git churn row: the bytes of git log output that the churn stage read.
Use --performance when:
- The analysis is slower than you expect, and you want to find the slow stage.
- You want to check that the cache works. A high hit rate means that incremental runs are fast.
- You want to compare the speed of different configuration options.
Parallel parse timing
parse/extract runs on all cores. When the run parses many files in parallel, the line gets a (parallel: ~Nms CPU) suffix. The suffix is the total parse CPU time of all workers:
│ parse/extract: 382.4ms (21033 modules, 0 cached, 21033 parsed) (parallel: ~594ms CPU)
Here the stage took 382ms of wall-clock time and ~594ms of CPU time across cores. If the CPU time is much larger than the wall-clock time, the stage is CPU-bound and more cores make it faster. If the two are close, the stage is I/O-bound and more threads do not help. Fallow omits the suffix on warm or small runs, where there is little parse work.
In combined mode (fallow with no subcommand), the health breakdown reuses the files that the dead-code pass discovered and parsed. Its discover files and parse/extract rows therefore show (measured above). You find that cost in the Pipeline Performance box.
With --format json, the timings include parse_cpu_ms (total parse CPU time) and parse_cache_load_ms (the parse cache load, part of parse_extract_ms) next to the wall-clock fields. The health timings include shared_parse. These values change from run to run, so do not use them as a CI gate.
The JSON timings also carry these objects:
counters: the work counts, with the fieldsfiles_read,source_bytes_read,parse_cache_bytes_read,resolve_specifier_calls,unique_specifiers,oxc_resolve_callsandcanonicalize_calls. The health timings carrygit_log_bytes. These counts are stable, so a test can compare them exactly.process: the process time in milliseconds, with the fieldswall_ms,startup_ms,thread_pool_ms,config_ms,git_ms,analysis_ms,post_analysis_msandoutput_ms.spans: the span tree. Each span has aname, theparentspan that holds it, itsms, andconcurrent, which istruewhen the span runs at the same time as its siblings. The tree shows that file discovery, parsing and the cache update are siblings of thepipelinespan, which is theTOTALclock.
Cache behavior
Fallow caches the parse result of each file, so later runs parse only the files that changed. The cache uses bincode serialization and xxh3 hashing.
# Run with caching (default)
fallow dead-code
# Skip the cache entirely
fallow dead-code --no-cache
Use --no-cache when:
- You think that stale cache entries cause incorrect results.
- You changed the fallow version and want a clean analysis.
- You want to benchmark a full parse.
--performance shows cache statistics, including the number of cache hits and misses. On incremental runs, expect a high hit rate, because fallow parses only the changed files again.
A refused cache says so
When a run has a cache but cannot reuse it, fallow prints the reason under the stage that did the extra work. You can therefore tell a refused cache from a first run:
│ parse/extract: 16.2ms (335 modules, 0 cached, 335 parsed) (parallel: ~98ms CPU)
│ parse cache not reused: cache format version changed
The module-graph cache reports a refusal on its own line, graph cache not reused, under build graph. Neither line appears when fallow reuses the cache. Neither line appears with --no-cache either, because you asked for a cold run. fallow doctor reports the same two states before you run an analysis.
Common false positives
Dynamic imports with runtime values
Fallow resolves template literals and import.meta.glob. It cannot resolve a fully dynamic import such as import(variable).
Fix: Add the target directory to entry in your config:
{
"entry": ["src/plugins/*.ts"]
}
Convention-based exports
Some frameworks use exports by their name, with no import. Examples are Next.js generateStaticParams and Remix loader functions.
Fix: The built-in plugins handle most frameworks. For a less common framework, create a custom plugin or use ignoreExports:
{
"ignoreExports": [
{ "file": "src/routes/**/*.ts", "exports": ["loader", "action"] }
]
}
Dependency injection frameworks
DI containers (NestJS, Angular, InversifyJS) resolve dependencies at runtime through decorators and metadata. Fallow skips decorated class members by default. An injected service can still look unused if only DI tokens reference it.
Fix: Add the DI-registered files as entry points, or suppress specific findings:
// fallow-ignore-next-line unused-export
export class UserService { /* ... */ }
Some decorators do NOT mean that a framework calls the method, for example Playwright @step or your own @measure and @log. List them in ignoreDecorators. Fallow then checks a method that has ONLY those decorators like any other method:
// .fallowrc.json
{ "ignoreDecorators": ["@step"] }
Fallow still skips a method that has any decorator NOT in the list. A method with @step + @Inject is still skipped, as DI code needs.
Peer dependencies and optional dependencies
Fallow does not report packages in peerDependencies, because the project that installs your package supplies them. Fallow does report a package in optionalDependencies that no file imports and no script uses. A package that loads at runtime without an import, such as a platform-specific binary, can therefore look unused.
Fix: If fallow reports such a package by mistake, add it to ignoreDependencies:
{
"ignoreDependencies": ["fsevents"]
}
Config files not recognized as entry points
If fallow reports a config file (for example tailwind.config.ts) as unused, its framework plugin is possibly not active.
Fix: Run fallow list to see the active plugins. If fallow does not detect the plugin, add the config file to entry:
{
"entry": ["tailwind.config.ts"]
}
Missing findings
Files in a hidden directory are missing
If a file in a dot-prefixed directory never appears in a finding, fallow did not look in that directory. Fallow skips hidden directories, except for a few conventional names and the directories that an active framework plugin owns. Check the diagnostics of the run:
fallow dead-code --format json --quiet | jq '.workspace_diagnostics[] | select(.kind == "skipped-source-dotdir")'
Fix: No config field adds a hidden directory to discovery. If a file, an
export or a dependency that only the hidden directory uses reports as unused,
add the file to entry, the export to ignoreExports or the dependency to
ignoreDependencies. To analyze the directory itself, run
fallow dead-code --root .claude. That run is separate and does not fix the
main run. If the directory holds tool state that you never want to analyze, add
it to ignorePatterns. For the full behavior, see
Known limitations.
A health section reports nothing
When a health input does not load, the section that needs it reports nothing,
not zero. The run records the reason as a diagnostic entry. Each entry has a
message that ends with a next step. Fallow records the entry with or without
--quiet. To find these entries, select on degrades_analysis, not on a list
of kinds:
fallow health --format json --quiet | jq '.workspace_diagnostics[] | select(.degrades_analysis)'
kind | What it means | Degrades analysis |
|---|---|---|
file-scores-unavailable | The per-file score pass failed, so the score list is empty and every number that comes from the scores is zero | Yes |
hotspots-skipped | The hotspot, churn and ownership sections report nothing. cause names the input that stopped them: not-a-repository, invalid-since or churn-file-unreadable | Yes |
shallow-clone | Churn covers the fetched history only. ownership_requested says if you asked for ownership, which a shallow clone skews too | Yes |
unpinned-clock | No commit timestamp, so churn recency and ownership staleness drift between two runs over the same commit | Yes |
ownership-unavailable | The ownership inputs did not load, so the owner signals are degraded or absent. cause is invalid-bot-pattern or codeowners-parse-failed | Yes |
trend-snapshot-unreadable | Fallow could not read a saved snapshot, so the trend direction uses fewer points than the project has on disk | Yes |
coverage-auto-detected | Provenance, not a failure. Fallow found coverage on disk instead of --coverage, and path names the file that fed the CRAP scores | No, the field is absent |
plugin-config-unreadable | A framework plugin could not read a config key in full, so what the key declares never reached the analysis. plugin, key and reason name the plugin, the config key and the cause, with the config file in path. For a Module Federation runtime call with an argument that is not a static literal, path is the source file and reason is dynamic-argument. reason is not-object-literal, array-form, spread, unreadable-entries or dynamic-argument | Yes |
plugin-effect-not-modeled | A framework plugin read a config key whose effect fallow does not model, so the convention remains. The same payload, with reason key-effect-not-modeled or config-property-unreadable | No, the field is absent |
The two plugin kinds come from a config file, not from a health input. Every
run with a dead-code pass can include them: dead-code / check, health,
flags, audit and the bare combined run. A standalone fallow dupes includes
neither. The reason token sets are open too. For the keys that report one,
see Module Federation.
Fix: Read the message of the entry. fallow health shows
which sections report nothing, and what a section reports when it measured zero.
The kind set is open. Treat a kind that you do not recognize as one more
diagnostic, not as an error.