Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Dart (3.13)  Module 10 – Errors and exceptions

Error handling design and input validation

Choose between exceptions, nullable returns and sealed results in Dart, check untrusted input in a safe order and keep internals out of messages.

  • Intermediate
  • 15 minutes
  • Examples run with dart 3.13.5 and Python 3.14.8
  • By MySmartCoPilot
Free core, Pro depth: 5 of 15 minutes free
Free: One rule, three ways to report that it failed · Choose by what the failure means to its caller. With a pass: 2 in-depth parts and 2 interview answers, about 10 minutes.

What you will learn

  • Choose between an exception, a nullable return and a sealed result for each kind of failure
  • Validate untrusted input with a size limit, a type check and an allowlist, in that order
  • Write error messages that tell a person what to fix while the details go to a log
  • Map every failure of a command line program to an exit code at one boundary

Before you start

What you’ll learn · Before you start

What you will learn

  • Choose between an exception, a nullable return and a sealed result for each kind of failure
  • Validate untrusted input with a size limit, a type check and an allowlist, in that order
  • Write error messages that tell a person what to fix while the details go to a log
  • Map every failure of a command line program to an exit code at one boundary

Before you start

On this page

A signup form can fail in many ways: a username of one letter, a name of 1 MB pasted by a script, a disk that is full, a bug in the code that saves the account. Error handling design is the set of decisions about each one: does the code throw it or return it, what does the person at the keyboard read, and what does the log keep?

This lesson makes those decisions for a small Dart signup. Every program in it ran on Dart 3.13.5, and the first decision is the one every check faces: how to report that it failed.

One rule, three ways to report that it failed

A username must be 3 to 20 characters from a-z, 0-9 and the underscore. This program reports a broken rule in each of the three ways Dart offers, and calls each one on the same three inputs:

three_ways.dart: one rule as an exception, a nullable and a sealed result Dart · three_ways.dart
// One username rule, reported three ways: an exception, a nullable return and a sealed result type.
final _shape = RegExp(r'^[a-z0-9_]+$');

String? _problem(String s) {
  if (s.length < 3 || s.length > 20) return 'must be 3 to 20 characters, not ${s.length}';
  if (!_shape.hasMatch(s)) return 'may hold only a-z, 0-9 and _';
  return null;
}

// 1. An exception: the caller must remember to catch it.
class UsernameException implements Exception {
  UsernameException(this.reason);
  final String reason;
}

String checkUsername(String s) {
  final problem = _problem(s);
  if (problem != null) throw UsernameException(problem);
  return s;
}

// 2. A nullable return: the type forces a null check, but null says nothing about why.
String? usernameOrNull(String s) => _problem(s) == null ? s : null;

// 3. A sealed result: the type forces a switch, and the refusal carries its reason.
sealed class UsernameCheck {}

class Accepted extends UsernameCheck {
  Accepted(this.name);
  final String name;
}

class Refused extends UsernameCheck {
  Refused(this.reason);
  final String reason;
}

UsernameCheck usernameCheck(String s) {
  final problem = _problem(s);
  return problem == null ? Accepted(s) : Refused(problem);
}

void main() {
  for (final input in ['asha_k', 'A', 'ravi.kumar']) {
    print('"$input"');
    try {
      print('  exception: accepted ${checkUsername(input)}');
    } on UsernameException catch (e) {
      print('  exception: refused, ${e.reason}');
    }
    print('  nullable:  ${usernameOrNull(input) ?? 'null, with no reason'}');
    final shown = switch (usernameCheck(input)) {
      Accepted(:final name) => 'accepted $name',
      Refused(:final reason) => 'refused, $reason',
    };
    print('  result:    $shown');
  }
}

Output

"asha_k"
  exception: accepted asha_k
  nullable:  asha_k
  result:    accepted asha_k
"A"
  exception: refused, must be 3 to 20 characters, not 1
  nullable:  null, with no reason
  result:    refused, must be 3 to 20 characters, not 1
"ravi.kumar"
  exception: refused, may hold only a-z, 0-9 and _
  nullable:  null, with no reason
  result:    refused, may hold only a-z, 0-9 and _

Recorded with Dart SDK 3.13.5 (stable) on macos_arm64 on macOS 26 arm64. To run it yourself: dart run three_ways.dart

All three accepted asha_k and refused the other two. The exception carried the reason, but nothing in checkUsername’s type told its caller to catch it. The nullable return forced a null check through its type String?, yet A and ravi.kumar both came back as a bare null: the reason was lost. The sealed class: A class marked sealed: it cannot be extended or implemented outside its own library, so the compiler knows all its direct subtypes and can check that a switch over them is exhaustive. More in “Validating JSON in Dart with patterns” UsernameCheck did both. Its switch has to name Accepted and Refused before it compiles, and Refused holds the reason, the pattern that validating JSON with patterns used for request bodies.

Checkpoint

Check: Three ways to report a failure

  1. Question 1 of 2 three_ways.dart checks the input A in all three styles. Which style could not say why A was refused?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The nullable return

    Only the exception and the sealed result carry a reason; the nullable return only says no.

  2. Question 2 of 2 Which of the three styles makes the compiler refuse a caller that ignores the failure case?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The sealed result, and the nullable return

    A type that holds the failure is checked by the compiler; a thrown exception is not part of the type.

Choose by what the failure means to its caller

None of the three is always right. The choice follows from what the failure means: is it part of normal use, does the caller need the reason, and how far away is the code that can act on it? Answer those questions in the table:

Interactive · answer and compare Exception, nullable return or sealed result? Answer three questions about the failure you are designing for. The table marks what fits and says why.
Is this failure part of normal use, such as a typo in a form, rather than a full disk or a lost connection?
Does the caller need to know why it failed, to show it or to act on it?
Is the code that can act on the failure many calls away from the check?

Answer the questions above: the table then marks what fits your case and says why the rest does not.

Three ways to report a failed check, against four things a caller needs
Way to report it Callers must handle it It says why It crosses many calls It suits a normal outcome
Throw an exception The caller writes: try and on UsernameException Poor nothing in the type: a caller can forget the try and the program stops Good UsernameException carries the reason the rule gave Good travels up through every caller until a catch takes it Poor meant for failures outside normal use, not for every typo
Return null The caller writes: ?? or an if that checks for null Good String? makes every caller deal with null before using the value Poor null only says no; three_ways.dart could not tell A from ravi.kumar Mixed each layer must pass the null on or decide what it means Good the lightest choice when absence is the only outcome
Return a sealed result The caller writes: a switch over Accepted and Refused Good an exhaustive switch does not compile without a Refused case Good Refused holds the reason, and more fields if the caller needs them Mixed each layer returns it on, which makes the failure visible in every signature Good made for outcomes that are part of normal use
How the answers decide
  • “Is this failure part of normal use, such as a typo in a form, rather than a full disk or a lost connection?” No, it means something broke: rules out Return null and Return a sealed result. A failure outside normal use should not sit in every return type; throw an exception and catch it where something can be done.
  • “Is this failure part of normal use, such as a typo in a form, rather than a full disk or a lost connection?” Yes, it happens every day: rules out Throw an exception. An everyday outcome is not exceptional; a caller that forgets a try would stop the program on a typo.
  • “Is this failure part of normal use, such as a typo in a form, rather than a full disk or a lost connection?” Yes, it happens every day and “Does the caller need to know why it failed, to show it or to act on it?” Yes, the reason matters: rules out Return null. null cannot say which rule failed, so the caller could not show the reason.
  • “Is this failure part of normal use, such as a typo in a form, rather than a full disk or a lost connection?” Yes, it happens every day and “Does the caller need to know why it failed, to show it or to act on it?” No, absence is enough: adds a caveat to Return a sealed result. When absence is the only outcome, such as a lookup that finds nothing, a nullable return does the job with less code.
  • “Is this failure part of normal use, such as a typo in a form, rather than a full disk or a lost connection?” Yes, it happens every day and “Is the code that can act on the failure many calls away from the check?” Yes, several layers up: adds a caveat to Return a sealed result and Return null. Every layer between the check and the handler must return the value on; that is visible in the types, and it is work.

The short version: throw an exception: An object a Dart program throws to report a failure it is expected to handle, such as text that does not parse or a file that is missing. Classes that implement Exception can be caught by type with an on clause. More in “Dart exceptions and errors: try, catch, finally” for a failure outside normal use, such as a full disk or a lost connection, and let a caller several layers up catch it. Return null when absence is the only outcome and needs no reason, such as a lookup that finds no user. Return a result type: A type whose values are either a success holding a value or a failure holding a reason, such as Ok and Bad, so that expected failures are returned instead of thrown. More in “Validating JSON in Dart with patterns” when the failure is an everyday outcome whose reason the caller must show, such as a refused form field. A bug is none of these: it stays an Error that reaches the top.

Good error handling design also decides what happens around those calls. Input validation decides the order of the checks on untrusted input: Data that comes from outside the program, such as a request body, a file or a message, and may have any shape or content until the program has checked it. More in “Validating JSON in Dart with patterns”: its size first, then its type, then an allowlist: A list of what is accepted, such as the field names an endpoint takes. Anything not on it is refused, instead of trying to list everything that should be blocked. More in “Validating JSON in Dart with patterns” of the characters it may hold. An assert cannot do input validation, because dart run ignores assertion: A check written as assert(condition, message) that throws an AssertionError when the condition is false, but only while assertions are enabled, as with --enable-asserts or in Flutter debug mode.. A boundary at the edge of the program turns each failure into a message, a log line and an exit code.

Key idea

Throw for failures outside normal use, return null when absence needs no reason, and return a sealed result when the caller must show or act on the reason.

Checkpoint

Check: Choosing by what a failure means

  1. Question 1 of 2 saveAccount fails when the disk is full, which happens a few times a year. How should it report that?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Throw an exception, caught where the program can tell the person to try later

    Exceptions fit failures outside normal use that travel far; results fit everyday outcomes.

  2. Question 2 of 2 findUserByEmail looks up an email that may not belong to anyone. The caller only needs to know whether a user exists. Which return type fits?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: User?

    Null fits when absence is the only outcome and needs no explanation.

Validating untrusted input in a safe order, with measured runs Pro: opens with a Pro, Premium or Ultimate pass

Untrusted text can be long, malformed or written to break a check. This part puts the checks in the order that costs least, measures what a size limit saves, compares three rules on real names and shows why assert cannot guard input.

In this part:

  • Check the size first: a 1 MB name costs nothing to refuse
  • An allowlist accepts the real names a denylist refuses
  • assert is not input validation

1 diagram · 4 runnable examples (Dart, Python) · about 7 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Messages, logs and one boundary that sets the exit code Pro: opens with a Pro, Premium or Ultimate pass

A person needs to know what to do next; whoever fixes the system needs the details, and neither needs the other’s text. This part splits one failure between the two audiences, then gives a command line program one boundary that decides every message and exit code.

In this part:

  • A message for the person, the details for the log
  • One boundary in main maps each failure to an exit code

1 diagram · 3 runnable examples (Dart, Python) · about 6 minutes

Opening this part with your pass…Pro part. It opens with the same pass as the part above.

Practise

The self-check on reporting a failure and choosing how starts with whichever of the two ideas you know less well, and a question you miss returns once before the session ends:

Practice that adapts to you

Practise this lesson

5 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 5 What does a caller of String? usernameOrNull(String s) get when the rule fails?

    Choose one answer.

    Show the answer: Question 1 of 5

    Answer: null, and nothing about why

    three_ways.dart printed null, with no reason, for both refused inputs.

  2. Question 2 of 5 A caller switches over UsernameCheck with a case for Accepted only. What happens?

    Choose one answer.

    Show the answer: Question 2 of 5

    Answer: It does not compile, because the switch is not exhaustive

    That compile error is what makes a sealed result force callers to handle the refusal.

  3. Question 3 of 5 A coupon field refuses about one code in ten, and the form must show why each code was refused. Which style fits?

    Choose one answer.

    Show the answer: Question 3 of 5

    Answer: A sealed result whose refusal carries the reason

    Everyday outcomes with a reason are what a sealed result type is for.

  4. Question 4 of 5 When is a nullable return the right way to report that nothing was found?

    Choose one answer.

    Show the answer: Question 4 of 5

    Answer: When absence is a normal answer and the caller needs no reason

    A lookup that finds no user is the typical case.

  5. Question 5 of 5 A helper sometimes calls first on an empty list and throws a StateError. A teammate wants it to return a sealed Failed result instead. What do you answer?

    Choose one answer.

    Show the answer: Question 5 of 5

    Answer: Fix the code so it never asks an empty list for its first element; a bug is not an outcome to return

    Errors stay errors: they reach the top with their trace, and the code gets fixed.

Interview questions

Warm-up (2-5 years): When should a Dart function return null instead of throwing?

Return null when “nothing there” is a normal answer and the caller needs no reason: a lookup by id that finds no user, a cache miss, an optional setting that is not set. The nullable return type then makes every caller handle the absent case before using the value, which an exception does not. Throw instead when the failure is outside normal use, such as a lost connection or a corrupt file, or when the caller must know why it failed: a bare null cannot tell one cause from another, which is when a sealed result type with a reason fits better.

Three longer questions put these choices to work on real designs:

  1. Mid to senior

    A ticketing library's parseBookingRef(String text) throws FormatException for every bad input, and each app shows "invalid reference". Support says users cannot tell a typo from an expired code. Redesign the API so callers can show the right message for each problem without making the common case harder.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    A signup endpoint checks names with a regular expression full of nested groups. A script sends names of 2 MB, and each request now takes about 4 seconds of CPU. Fix the validation, explain the order of the checks, and say how you would test the fix.

    Model answer, Pro. First step and follow-ups

Flashcards · 6 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. What are the three ways a Dart function can report a broken rule?

    Throw an exception, return null, or return a sealed result. Only the sealed result both makes the caller handle the failure and carries the reason.

  2. What does a thrown exception not tell the caller of checkUsername?

    That it has to catch anything: nothing in the function's type says so. The exception carries the reason, but only a sealed result's switch has to name every outcome before it compiles.

  3. Why can a nullable return be the wrong way to report a refused username?

    Its String? type forces a null check, but 'A' and 'ravi.kumar' both came back as a bare null: the reason was lost.

  4. When should code throw, return null or return a sealed result?

    Throw for a failure outside normal use, such as a full disk or a lost connection; return null when absence needs no reason, such as a lookup that finds no user; return a sealed result when the caller must show or act on the reason.

  5. Should a bug ever come back as a result value?

    No. A bug is none of the three reports: it stays an Error that reaches the top, where the code gets fixed.

  6. In which order should code check untrusted input?

    Its size first, then its type, then an allowlist of the characters it may hold. An assert cannot do this, because dart run ignores assertions.

Key takeaways

  • One failed check can be reported as an exception, a nullable return or a sealed result, and only the sealed result both forces the caller to handle it and carries the reason.
  • Throw for failures outside normal use, return null when absence needs no reason, and return a sealed result for everyday outcomes the caller must explain.
  • Errors that report bugs are never returned as results: they reach the top, where the code gets fixed.

Terms in this lesson

assertion
A check written as assert(condition, message) that throws an AssertionError when the condition is false, but only while assertions are enabled, as with --enable-asserts or in Flutter debug mode.
denylist
A list of values or characters that a check refuses, with everything else accepted. It guesses what bad input looks like, the opposite of an allowlist.
error boundary
One place near the edge of a program, such as a try in main, where failures from the code below are turned into messages, log lines and exit codes or responses.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.