Debugging a Dart Build Generator#
Ten concrete techniques for diagnosing failures in build_runner / source_gen
generators.
1. Run with verbose logging#
The single most useful first step. Verbose mode prints every builder invocation, asset resolution, and cache decision.
dart run build_runner build --verbose
For watch mode:
dart run build_runner watch --verbose --delete-conflicting-outputs
Look for lines prefixed with the builder name to see exactly what inputs it received and what output it tried to write.
2. Delete the build cache#
Stale cache entries cause phantom failures — the builder never reruns because build_runner thinks nothing changed.
dart run build_runner clean
dart run build_runner build
Do this whenever:
- Output is missing but no error is shown
- Old generated code persists after you changed the generator
- You renamed or moved files
3. Attach a real Dart debugger#
build_runner spawns a child process to run the build. Debugger flags must be passed through to that child process via
--dart-jit-vm-arg — you cannot attach to the parent dart run build_runner process directly.
Start the build with the VM service enabled#
dart run build_runner build \
--dart-jit-vm-arg=--observe \
--dart-jit-vm-arg=--pause-isolates-on-start
--observestarts the Dart VM service (defaults to port 8181)--pause-isolates-on-startholds the child process until a debugger attaches
The child process prints a DevTools URL:
The Dart DevTools debugger and profiler is available at:
http://127.0.0.1:8181/3xXtAPE8msc=/devtools/?uri=ws://127.0.0.1:8181/3xXtAPE8msc=/ws
Attach from your IDE#
VS Code: open the Command Palette → Debug: Attach to Dart Process → paste the URL above.
IntelliJ / Android Studio: Run → Attach to Process → paste the VM service URL.
Dart DevTools (browser): open the devtools/ URL directly in Chrome.
Set breakpoints before attaching#
Place breakpoints in your generator's generate() or generateForAnnotatedElement()
method before attaching. Because --pause-isolates-on-start holds execution, the process waits until you resume from the debugger.
Custom VM service port#
If port 8181 is in use:
dart run build_runner build \
--dart-jit-vm-arg=--observe=8182
watch mode#
Works the same way — use watch instead of build. The debugger session stays alive across rebuilds.
dart run build_runner watch \
--dart-jit-vm-arg=--observe \
--dart-jit-vm-arg=--pause-isolates-on-start
4. Use log instead of print in production, capture in tests#
The build package provides a zone-scoped logger. It surfaces warnings/errors even without
--verbose and is capturable in tests.
import 'package:build/build.dart';
// Inside build() or generate():
log.info('Processing ${buildStep.inputId}');
log.warning('Abstract class skipped: ${element.name}');
log.severe('Unexpected element kind: ${element.kind}');
Capture in tests:
final logs = <LogRecord>[];
await testBuilder(
myBuilder(BuilderOptions.empty),
{'pkg|lib/model.dart': source},
onLog: logs.add,
);
expect(logs.map((r) => r.message), contains(contains('skipped')));
5. Inspect the resolved element tree in the debugger#
Unexpected generation output usually means you misread the AST/element model. With the debugger attached (step 3), set a breakpoint at the top of
generate() or generateForAnnotatedElement() and inspect the live objects.
Useful watch expressions:
| Expression | What to look for |
|---|---|
library.allElements.toList() |
All top-level elements the analyzer resolved |
element.runtimeType |
Confirm it's ClassElementImpl, not a typedef or extension |
element.fields |
All fields including synthetic getters — filter with !f.isSynthetic |
element.constructors |
Check the unnamed constructor exists and has the expected params |
element.typeParameters | Non-empty for generic classes |
element.supertype | The resolved superclass (may be Object) |
Set a conditional breakpoint (e.g., element.name == 'MyModel') to stop only on the class you care about.
6. Inspect annotation values in the debugger#
If your generator reads annotation parameters and produces wrong output, set a breakpoint after annotated.annotation
is in scope and inspect the ConstantReader directly.
Useful watch expressions on reader (ConstantReader):
| Expression | What to look for |
|---|---|
reader.objectValue |
The raw DartObject — expand to see all fields |
reader.objectValue.fields |
Map of field name → DartObject value |
reader.peek('name') |
null if the field was not set (uses default) |
reader.peek('name')?.stringValue |
The actual string, or throws if not a string |
reader.peek('mode')?.revive().accessor | Enum constant name as a string |
Common pitfalls (visible once you can inspect live values):
-
read('field')throws if the field is absent — usepeek('field')to getnullinstead -
Enum values:
revive().accessorgives the constant name; match againstMyEnum.values - Nested annotations:
ConstantReader(reader.read('nested').objectValue)to recurse
7. Use testBuilder for fast isolated reproduction#
Running the full build_runner is slow and caches state. Reproduce the failing case in a unit test for a fast feedback loop.
import 'package:build_test/build_test.dart';
import 'package:test/test.dart';
void main() {
test('reproduces the bug', () async {
await testBuilder(
myBuilder(BuilderOptions.empty),
{
'pkg|lib/model.dart': '''
import 'package:my_annotations/my_annotations.dart';
part 'model.g.dart';
@MyAnnotation(name: 'custom')
class Model {
final String id;
}
''',
},
outputs: {
'pkg|lib/model.my_builder.g.part': decodedMatches(
contains('custom'),
),
},
);
});
}
Run with: dart test test/my_repro_test.dart
8. Check build.yaml configuration issues#
Most "my builder doesn't run" problems come from a misconfigured build.yaml.
builders:
my_builder:
import: "package:my_generator/builder.dart"
builder_factories: ["myBuilder"] # must match function name exactly
build_extensions: {".dart": [".my_builder.g.part"]}
auto_apply: dependents # runs for packages that depend on this one
build_to: cache # use 'source' only for standalone file builders
applies_builders: ["source_gen|combining_builder"] # required for SharedPartBuilder
Common mistakes:
| Symptom | Likely cause |
|---|---|
| Builder never runs | auto_apply: none or wrong package dependency |
| Output file not created | build_to: cache but checking source directory |
combining_builder output missing | applies_builders not set |
| Factory not found | builder_factories name mismatch with actual function |
Verify with: dart run build_runner build --verbose 2>&1 | grep -i "my_builder"
9. Handle InvalidGenerationSourceError with precise messages#
When the generator throws without a message, build_runner prints a generic stack trace. Throw
InvalidGenerationSourceError with context so the error is actionable.
import 'package:source_gen/source_gen.dart';
// Bad — produces an unhelpful stack trace
if (element is! ClassElement) throw Exception('not a class');
// Good — points to the offending element in the source file
if (element is! ClassElement) {
throw InvalidGenerationSourceError(
'@MyAnnotation can only be applied to classes. '
'Found: ${element.runtimeType} "${element.name}".',
element: element, // IDE and build output will link to this location
);
}
Test that errors are thrown correctly:
test('throws on enum target', () {
expect(
() => testBuilder(
myBuilder(BuilderOptions.empty),
{'pkg|lib/bad.dart': '@MyAnnotation()\nenum Bad { a }'},
),
throwsA(
isA<InvalidGenerationSourceError>().having(
(e) => e.message, 'message', contains('classes'),
),
),
);
});
10. Verify the generated output parses as valid Dart#
A generator that produces syntactically invalid Dart causes silent failures during the combining step — the
.g.dart file is never written or contains a syntax error that's hard to trace back to your generator.
import 'package:dart_style/dart_style.dart';
// Inside generate(), before returning:
String? _formatAndValidate(String code) {
final formatter = DartFormatter();
try {
return formatter.format(code);
} on FormatterException catch (e) {
log.severe('Generated code is not valid Dart:\n$e\n\n--- code ---\n$code');
rethrow;
}
}
In tests, assert the output is parseable:
import 'package:analyzer/dart/analysis/utilities.dart';
test('output is valid Dart', () async {
final writer = RecordingAssetWriter();
await testBuilder(myBuilder(BuilderOptions.empty), inputs, writer: writer);
final outputId = AssetId('pkg', 'lib/model.my_builder.g.part');
final content = utf8.decode(writer.assets[outputId]!);
// parseString throws ParseStringResult with errors if invalid
final result = parseString(content: content);
expect(result.errors, isEmpty, reason: 'Generated code has parse errors');
});