Chapter 8

The Error System

Turning crashes into teaching moments

Most languages, when something goes wrong, hand a beginner a stack trace and a cryptic message. Bloom takes the opposite approach: every error tries to guess what you meant and explain the fix in plain language. All of this machinery lives in src/lang/errors.ts.

An error in Bloom is more than a message. It is a small structured object:

src/lang/errors.ts
export interface ErrorSuggestion {
  message: string;            // What went wrong
  suggestion: string | null;  // "Did you mean ...?"
  hint: string | null;        // A plain-language nudge
  documentation: string | null; // Usage docs for a built-in
}

The three optional fields are what make Bloom errors feel friendly. formatErrorWithSuggestion() stacks them into the final text the playground shows.

Typo Detection with Levenshtein Distance

When you call a function that doesn't exist, Bloom doesn't just say "undefined." It measures how close your typo is to every name it knows and offers the nearest match. The measure is the classic Levenshtein edit distance — the number of single-character insertions, deletions, or substitutions needed to turn one word into another.

src/lang/errors.ts — levenshteinDistance()
matrix[i][j] = Math.min(
  matrix[i - 1][j - 1] + 1, // substitution
  matrix[i][j - 1] + 1,     // insertion
  matrix[i - 1][j] + 1      // deletion
);

findClosestMatch() runs that distance against a candidate list and accepts a match only if the distance is within a small threshold (2 for undefined variables), so unrelated names never produce nonsense suggestions:

circel(...)
Distance 2 from circle"Did you mean 'circle'?" plus its usage docs
bckground(...)
Distance 1 from background → suggested, with background(color) documentation
xyzzy(...)
No candidate within distance 2 → falls back to a generic "did you declare it?" hint

The candidate list comes from getAllKnownIdentifiers(), which unions every built-in function (BUILTIN_FUNCTIONS), built-in variable (BUILTIN_VARIABLESmouseX, frame, width …), and named color (NAMED_COLORS). At runtime the interpreter also passes in the program's own declared variables, so suggestions can point at names you defined.

Cross-Language "False Friends"

Many beginners arrive from Python or JavaScript and reach for keywords that don't exist in Bloom. createUndefinedVariableError() special-cases the most common ones before it even tries fuzzy matching:

You typed
Bloom says
function / def
Use fn instead
var / const
Use let instead
elif
Use else if
null / None / undefined
Use nil
True / False
Use lowercase true / false

There is also a dedicated path for the two functions every sketch needs. If setup or draw is missing, the error includes a ready-to-paste skeleton — for example fn setup() { size(400, 400) }.

Arity and Type Errors

createArityError() fires when a built-in gets the wrong number of arguments. Because BUILTIN_FUNCTIONS stores each function's parameter list, the error can show the exact signature you should have used:

circle expects 3 argument(s) but got 2

  Check the arguments: circle(x, y, radius)
  You might be missing some arguments
  Usage: circle(x, y, radius) - Draw a circle

createTypeError() translates raw type names into beginner phrasing — number becomes "a number (like 42 or 3.14)", array becomes "a list (like [1, 2, 3])" — and tailors the hint to the operation. Adding text to a number, indexing with a decimal, and iterating over a non-array each get their own targeted advice.

Catching Mistakes Before Running

Some mistakes are valid syntax but almost certainly wrong. checkCommonMistakes() scans the raw source line-by-line against a table of regular-expression patterns (COMMON_MISTAKES) and surfaces warnings without stopping the program. It strips line comments first so a // note never trips a pattern.

x && y
Use and instead of && for logical AND
a === b
Use == for equals (not ===)
circle(100, 100)
circle() needs 3 numbers — you're missing the radius
print "hi"
print needs parentheses: print("hi")

A pattern can carry a suppressIf regex so a per-line heuristic can use whole-program context — for instance, "missing size()" stays quiet if a size( call appears anywhere in the file. These warnings are wired into both backends in index.ts, so you get the same nudges whether your sketch runs on the VM or the interpreter.

Two stages, one philosophy The lexer and parser also produce friendly errors at their own stages — see the lexer (it rewrites ! into a "use not" message and explains .5 vs 0.5) and the parser (a bare = inside an if condition becomes "did you mean ==?"). errors.ts handles the runtime and whole-program layer.

Where It's Used

In the Source Code

Everything in this page lives in src/lang/errors.ts. Key exports: