A Dart CSV importer must report every bad row without aborting the whole file; how would you structure its exception handling?
answer
- row boundary versus file boundary
- validate before you parse
- tryParse over catching FormatException
- RowException with row and column
- Errors still crash the import
basics
~20 sIn a Dart importer, give each row its own try with on RowException, record row, column and message in a report and continue; let file-level exceptions end the import, and never catch Error, which means the importer is buggy.
solid answer
~50 sI split the failures by scope. A bad row is expected input, so each row gets its own `try` with `on RowException catch (e)`, and the handler appends the row, column and message to a report and moves on. Inside the row parser I validate before I parse — check `cells.length` rather than letting `cells[2]` throw a `RangeError`, and use `int.tryParse` so a bad number becomes a `RowException` without a throw-and-catch. Where a parser such as `DateTime.parse` throws `FormatException`, I translate it into a `RowException` with the column name. A missing or unreadable file is a `FileSystemException` at the file boundary: it aborts the import and propagates with its trace, cleaned up in `finally`. I never catch `Error` in the loop; a `StateError` or `TypeError` there is my bug, and hiding it would silently mis-import data.
code
dart · 54 linesimport 'dart:io';
class RowException implements Exception {
const RowException(this.row, this.column, this.message);
final int row;
final String column;
final String message;
@override
String toString() => 'row $row, $column: $message';
}
class Item {
const Item(this.name, this.quantity, this.delivered);
final String name;
final int quantity;
final DateTime delivered;
}
class ImportReport {
final items = <Item>[];
final issues = <RowException>[];
}
Item parseRow(int row, String line) {
final cells = line.split(',');
if (cells.length != 3) {
throw RowException(row, '*', 'expected 3 cells, got ${cells.length}');
}
final quantity = int.tryParse(cells[1].trim());
if (quantity == null) {
throw RowException(row, 'quantity', 'not an integer: "${cells[1]}"');
}
final DateTime delivered;
try {
delivered = DateTime.parse(cells[2].trim());
} on FormatException catch (e, st) {
Error.throwWithStackTrace(RowException(row, 'delivered', e.message), st);
}
return Item(cells[0].trim(), quantity, delivered);
}
ImportReport importCsv(File file) {
final report = ImportReport();
final lines = file.readAsLinesSync(); // file-level failure: propagates
for (var i = 1; i < lines.length; i++) {
try {
report.items.add(parseRow(i + 1, lines[i]));
} on RowException catch (e) {
report.issues.add(e); // row-level failure: recorded, loop continues
}
}
return report;
}go deeper
Recall that each row needs its own try with an on clause for the row failure, so one bad row does not stop the rest.
Explain validating with tryParse and length checks, translating FormatException into one domain exception, and why the file read sits outside the row try.
Show the policy by scope: row failures are reported, file failures abort with their trace, Errors are never caught, and translation keeps the cause.
Weigh partial import against all-or-nothing, issue caps against complete reports, and who sees traces versus user-facing row messages.
## Two boundaries, two policies An importer meets failures at two different scopes, and the **scope decides the policy**. Mixing them up gives either an importer that dies on row 3 of 10,000 or one that reports "0 rows imported, no errors" after swallowing a bug. | Failure | Typical type | Scope | Policy | |---|---|---|---| | wrong cell count, bad number, bad date | your `RowException`, or `FormatException` translated into one | one row | record in the report, continue with the next row | | file missing or unreadable | `FileSystemException` | whole file | abort, propagate to the caller with the trace | | indexing past a row's end, a failed cast, a failed `assert` | `RangeError`, `TypeError`, `AssertionError` | the importer's code | do not catch; fix the code | ## Turn bad input into exceptions, not errors A short row that makes `cells[2]` throw `RangeError` is **bad input surfacing as a bug**, because the importer skipped a check it could have made. Catching `RangeError` would work today and hide the next real indexing bug tomorrow. So the row parser **validates first**: 1. Split the line and check `cells.length` against the header. 2. Parse numbers with `int.tryParse` / `double.tryParse`, which return `null` instead of throwing, and turn `null` into a `RowException` naming the column. 3. Where only a throwing parser exists — `DateTime.parse` throws `FormatException` — catch **that type only** and translate it into a `RowException` so the loop has one type to handle. The result is a single **domain exception** — `RowException implements Exception`, with `row`, `column` and `message` fields — that means exactly "this row is bad". ## The row loop ```dart ImportReport importCsv(File file) { final report = ImportReport(); final lines = file.readAsLinesSync(); // FileSystemException propagates for (var i = 1; i < lines.length; i++) { try { report.items.add(parseRow(i + 1, lines[i])); } on RowException catch (e) { report.issues.add(e); } } return report; } ``` Notes on the shape: - The `try` wraps **one row**, so one failure cannot skip its neighbours. - The clause is **`on RowException`**, not a bare `catch`, so a bug in `parseRow` still stops the import with a trace pointing at it. - The file read sits **outside** the loop's `try`, so a file-level failure is not mistaken for a row failure. - If the importer holds a resource — an open `RandomAccessFile`, a database transaction — release it in a `finally` around the whole import. ## Keeping context when you translate When you convert a `FormatException` from `DateTime.parse` into a `RowException`, keep the evidence. Either store the original as a `cause` field, or throw the new exception with `Error.throwWithStackTrace(rowException, st)` using the `st` from `catch (e, st)`, so a developer investigating a surprising report can still see where parsing failed. For the report shown to users, the row, column and message are what matter; the trace belongs in the developer log. ## Design choices a reviewer will probe - **Cap the issue list?** A file with every row broken — often the wrong delimiter — should stop early with one clear file-level message instead of 10,000 row reports. - **All or nothing?** Some imports must reject the whole file if any row fails; then the loop still collects every issue, but the caller commits nothing unless `issues` is empty. - **Where do Errors go?** Nowhere inside the importer. They reach the top-level reporting the app already has, which is owned by the app's own error-handling setup. - **What about exceptions nobody expected?** An `on Exception` clause at the file boundary can log and wrap them; a bare `catch` in the row loop should not exist.
- In a Dart importer, why prefer int.tryParse over int.parse inside try with on FormatException?Bad numbers are the expected case in an importer, and `int.tryParse` reports them as `null` without building and unwinding an exception. It keeps the parser's control flow plain, and the SDK's own `int.parse` docs point to `tryParse` instead of throwing and immediately catching.
- In a Dart importer, how would you handle a file where every row fails because the delimiter is wrong?Detect it early — for example the header has one cell, or the first N rows all fail on cell count — and abort with one file-level exception explaining the likely cause. Ten thousand identical row reports bury the real problem and waste the user's time.
- In a Dart importer, why does the per-row try use on RowException rather than on Exception?Only `RowException` means "this row is bad, keep going". A broader `on Exception` would also treat an unexpected `FileSystemException` or a library's own exception as a bad row, reporting it to the user as input trouble instead of stopping the import.
saying these in an interview costs you the question
- Wrap the whole loop in one catch (e) so nothing can crash the import.
- Catch RangeError to skip rows that have too few cells.
- A missing file should be reported as a bad first row and skipped.
- Log the message only; the stack trace adds nothing for input errors.
- Every exception inside the loop means the row is bad.