Skip to content
Fallow home
All docs pages

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.

  • startup is argument parsing and setup before the command runs. The thread pool is part of it.
  • config is the config load for the command.
  • git appears when --changed-since runs git.
  • analysis holds all the stage rows above it.
  • after analysis is the scope filters, rules, baselines and gates.
  • output is 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 fields files_read, source_bytes_read, parse_cache_bytes_read, resolve_specifier_calls, unique_specifiers, oxc_resolve_calls and canonicalize_calls. The health timings carry git_log_bytes. These counts are stable, so a test can compare them exactly.
  • process: the process time in milliseconds, with the fields wall_ms, startup_ms, thread_pool_ms, config_ms, git_ms, analysis_ms, post_analysis_ms and output_ms.
  • spans: the span tree. Each span has a name, the parent span that holds it, its ms, and concurrent, which is true when the span runs at the same time as its siblings. The tree shows that file discovery, parsing and the cache update are siblings of the pipeline span, which is the TOTAL clock.

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)'
kindWhat it meansDegrades analysis
file-scores-unavailableThe per-file score pass failed, so the score list is empty and every number that comes from the scores is zeroYes
hotspots-skippedThe hotspot, churn and ownership sections report nothing. cause names the input that stopped them: not-a-repository, invalid-since or churn-file-unreadableYes
shallow-cloneChurn covers the fetched history only. ownership_requested says if you asked for ownership, which a shallow clone skews tooYes
unpinned-clockNo commit timestamp, so churn recency and ownership staleness drift between two runs over the same commitYes
ownership-unavailableThe ownership inputs did not load, so the owner signals are degraded or absent. cause is invalid-bot-pattern or codeowners-parse-failedYes
trend-snapshot-unreadableFallow could not read a saved snapshot, so the trend direction uses fewer points than the project has on diskYes
coverage-auto-detectedProvenance, not a failure. Fallow found coverage on disk instead of --coverage, and path names the file that fed the CRAP scoresNo, the field is absent
plugin-config-unreadableA 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-argumentYes
plugin-effect-not-modeledA 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-unreadableNo, 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.

See also