Debugging & Common Mistakes

Every programmer runs into bugs. This page will help you understand Quill's error messages, use the built-in debugger, and avoid the most common pitfalls that trip up new and experienced Quill developers alike.

Reading Error Messages

Quill tries hard to give you helpful error messages. Instead of cryptic codes, you get a pointer to exactly where things went wrong, along with an explanation and often a suggestion for how to fix it.

Anatomy of an error message

Every Quill error has three parts:

  1. Location — the file name and line number
  2. Source context — the actual line of code with an underline showing the exact spot
  3. Hint — a suggestion for how to fix the problem

Let's look at five real error examples you will encounter and learn to read each one.

1. Type mismatch

You have a variable that should hold a number, but you gave it a string:

count is 0
-- ... later in the code ...
count is "hello"
Error [TYPE_MISMATCH] in counter.quill:4

  4 |  count is "hello"
               ^^^^^^^
  Type mismatch: 'count' was declared as Number but
  you are assigning a String.

  Hint: Did you mean to assign a number? If you need
        a string, declare a new variable instead.
What this means: Quill infers the type of count from its first assignment (0, a number). When you later try to assign a string to it, Quill catches the mismatch. Fix it by assigning a number, or use a different variable for the string value.

Fix

count is 0
-- ... later ...
count is 42
-- Or, if you need a separate string:
label is "hello"

2. Undefined variable

You used a name that hasn't been defined yet:

say greeting
Error [UNDEFINED] in hello.quill:1

  1 |  say greeting
           ^^^^^^^^
  'greeting' is not defined. You used this name but
  never assigned a value to it.

  Hint: Did you forget to write: greeting is "..."?
        Check for typos in the variable name.
What this means: Quill cannot find a variable named greeting anywhere in scope. This usually means you either forgot to define it, or you have a typo in the name.

Fix

greeting is "Hello, World!"
say greeting

3. Missing colon

You forgot the : at the end of a block statement:

if x is 5
  say "five!"
Error [SYNTAX] in check.quill:1

  1 |  if x is 5
                ^
  Expected ':' after condition. Block statements in
  Quill must end with a colon.

  Hint: Add a colon:  if x is 5:
What this means: In Quill, all block-starting statements (if, for each, to, while, etc.) need a colon at the end to signal that an indented block follows. This is similar to Python's syntax.

Fix

if x is 5:
  say "five!"

4. Wrong indentation

You mixed tabs and spaces, or used an inconsistent indent:

to greet name:
  say "Hello, {name}!"
	say "Welcome!"  -- this line uses a tab!
Error [INDENT] in greet.quill:3

  2 |    say "Hello, {name}!"
  3 |  	 say "Welcome!"
         ^
  Inconsistent indentation. Line 2 uses spaces but
  line 3 uses a tab character.

  Hint: Use spaces consistently (2 or 4 spaces).
        Configure your editor to insert spaces when
        you press Tab.
What this means: Quill uses indentation to determine code blocks (like Python). Mixing tabs and spaces confuses the parser. Pick one — we recommend 2 spaces — and stick with it.

Fix

to greet name:
  say "Hello, {name}!"
  say "Welcome!"

5. Import not found

The file you're trying to bring in doesn't exist:

bring "./helpers/math"
Error [IMPORT_NOT_FOUND] in app.quill:1

  1 |  bring "./helpers/math"
              ^^^^^^^^^^^^^^^^
  Could not find module './helpers/math'. Looked for:
    - ./helpers/math.quill
    - ./helpers/math/index.quill

  Hint: Check that the file exists and the path is
        correct. Paths are relative to the current file.
What this means: Quill looked for a file at the path you specified (with .quill extension added automatically) and couldn't find it. Double-check the path and make sure the file exists.

Fix

-- Make sure the file exists at ./helpers/math.quill
bring "./helpers/math"

-- Or check if the path should be different:
bring "./utils/math"

The Debugger

Quill ships with a built-in interactive debugger that lets you pause your program, step through code line by line, and inspect variables. It's one of the most powerful tools you have for finding bugs.

Starting the debugger

Run your file with the debug command instead of run:

quill debug app.quill

You'll see a prompt like this:

Quill Debugger v1.0
Loaded: app.quill (42 lines)
Paused at line 1.

(quill-debug) > _

Setting breakpoints

A breakpoint tells the debugger to pause when it reaches a specific line. Use the break command:

(quill-debug) > break 10
Breakpoint set at line 10.

(quill-debug) > break 25
Breakpoint set at line 25.

To see all your breakpoints:

(quill-debug) > breakpoints
  1: line 10 (app.quill)
  2: line 25 (app.quill)

To remove a breakpoint:

(quill-debug) > unbreak 10
Breakpoint at line 10 removed.

Stepping through code

Command Short What it does
step s Execute the current line and move to the next one
into i Step into a function call (follow the call)
out o Run until the current function returns
continue c Run until the next breakpoint (or end of program)

Inspecting variables

While paused, you can look at the current values of any variable:

(quill-debug) > print count
count = 7 (Number)

(quill-debug) > print name
name = "Alice" (String)

(quill-debug) > print items
items = ["apple", "banana", "cherry"] (List of String)

To see every variable in the current scope at once, use locals:

(quill-debug) > locals
  count   = 7 (Number)
  name    = "Alice" (String)
  items   = ["apple", "banana", "cherry"] (List)
  total   = 0 (Number)

Viewing the call stack

When you're inside a function, the stack command shows you how you got there:

(quill-debug) > stack
  #0  calculateTotal (app.quill:18)
  #1  processOrder (app.quill:32)
  #2  main (app.quill:45)

This reads from top to bottom: you're currently in calculateTotal at line 18, which was called by processOrder at line 32, which was called from the top level at line 45.

Full walkthrough: finding a bug

Let's debug a function that's returning the wrong result. Here's a program that should calculate the average of a list of numbers, but it's giving the wrong answer:

-- average.quill
to average numbers:
  total is 0
  for each num in numbers:
    total is total + num
  give back total / numbers.length

scores are [80, 90, 100]
result is average(scores)
say "Average: {result}"
-- Expected: 90, but getting: 60 ... why?

Let's fire up the debugger and find out:

$ quill debug average.quill

Quill Debugger v1.0
Loaded: average.quill (10 lines)
Paused at line 1.

(quill-debug) > break 5
Breakpoint set at line 5.

(quill-debug) > continue
Hit breakpoint at line 5.

  4 |    for each num in numbers:
  5 | >   total is total + num
  6 |    give back total / numbers.length

(quill-debug) > print num
num = 80 (Number)

(quill-debug) > print total
total = 0 (Number)

(quill-debug) > step
  5 | >   total is total + num

(quill-debug) > print total
total = 80 (Number)

(quill-debug) > continue
Hit breakpoint at line 5.

(quill-debug) > print num
num = 90 (Number)

(quill-debug) > print total
total = 80 (Number)

(quill-debug) > step

(quill-debug) > print total
total = 170 (Number)

(quill-debug) > continue
Hit breakpoint at line 5.

(quill-debug) > print num
num = 100 (Number)

(quill-debug) > step

(quill-debug) > print total
total = 270 (Number)

(quill-debug) > print numbers.length
numbers.length = 3 (Number)

-- Aha! 270 / 3 = 90. That's correct!
-- Let's continue to see the final output...

(quill-debug) > continue
Average: 90
Program finished.
Tip: In this walkthrough the code turned out to be correct. In practice, the debugger shines when you find that a variable has an unexpected value — that's the moment you know exactly where the bug is. Set breakpoints around the suspicious area, step through, and watch the values change.

Common Mistakes & Fixes

Here are the most common mistakes Quill programmers make. Don't feel bad if you run into these — everyone does, especially when learning a new language. Each entry shows the wrong code, the error you'll see, why it's wrong, and how to fix it.

1. Forgetting is for assignment

If you're coming from a language where assignment doesn't need a keyword, you might just write the variable name and value next to each other.

Wrong

name "hello"
Error [SYNTAX] in app.quill:1

  1 |  name "hello"
            ^^^^^^^
  Unexpected string literal. Did you mean to assign
  this value to 'name'?

  Hint: Use 'is' for assignment:  name is "hello"

Why it's wrong: In Quill, assignment always uses the is keyword. Just writing two things next to each other doesn't mean anything to the compiler.

Fix

name is "hello"

2. Using = instead of is

This is the single most common mistake for anyone coming from JavaScript, Python, or almost any other language. Muscle memory is hard to fight.

Wrong

count = 10
Error [SYNTAX] in app.quill:1

  1 |  count = 10
              ^
  Unexpected character '='. Quill uses 'is' for
  assignment, not '='.

  Hint: Write:  count is 10

Why it's wrong: Quill was designed to read like English. The = symbol doesn't exist in the language. Use is for assignment and is for comparison too — Quill figures out which you mean from context.

Fix

count is 10

3. Forgetting the colon

Block-starting lines like if, for each, to (function), and while all need a colon at the end.

Wrong

if x is 5
  say "five!"
Error [SYNTAX] in app.quill:1

  1 |  if x is 5
                ^
  Expected ':' after condition.

  Hint: Add a colon:  if x is 5:

Why it's wrong: The colon tells Quill that an indented block is about to follow. Without it, the parser doesn't know where the condition ends and the body begins.

Fix

if x is 5:
  say "five!"

4. Wrong indentation

Code that should be inside a block is placed at the wrong level, or you accidentally de-indented a line.

Wrong

to greet name:
  say "Hello, {name}!"
say "Welcome aboard!"   -- oops, this is outside the function!

What happens: No error is thrown, but "Welcome aboard!" runs immediately when the file loads, not when greet is called. This is a logic bug, not a syntax error, which makes it harder to spot.

Fix

to greet name:
  say "Hello, {name}!"
  say "Welcome aboard!"   -- now inside the function
Tip: Turn on "show whitespace" in your editor so you can see indentation characters. In VS Code, set editor.renderWhitespace to "all".

5. Using return instead of give back

In most languages you write return, but Quill uses the phrase give back.

Wrong

to add a, b:
  return a + b
Error [UNDEFINED] in math.quill:2

  2 |    return a + b
         ^^^^^^
  'return' is not defined. Did you mean 'give back'?

  Hint: Quill uses 'give back' to return values:
        give back a + b

Why it's wrong: Quill was designed with English readability in mind. Functions "give back" their results. The keyword return doesn't exist in Quill.

Fix

to add a, b:
  give back a + b

6. Comparing with == instead of is

Just like assignment, comparisons use is rather than ==.

Wrong

if name == "Alice":
  say "Hi Alice!"
Error [SYNTAX] in app.quill:1

  1 |  if name == "Alice":
               ^^
  Unexpected '=='. Quill uses 'is' for comparison.

  Hint: Write:  if name is "Alice":

Why it's wrong: There is no == operator in Quill. The word is handles both assignment and comparison. Quill determines which one you mean based on context: inside an if condition, is means comparison.

Fix

if name is "Alice":
  say "Hi Alice!"

7. Forgetting await on async functions

When you call an async function without await, you get a Promise object instead of the actual data.

Wrong

to fetchUser:
  response is await fetch("https://api.example.com/user")
  give back response.json()

-- Calling without await:
user is fetchUser()
say user.name   -- undefined! user is a Promise, not the data

What happens: No error is thrown, but user holds a Promise object rather than the actual user data. Trying to read user.name gives undefined, which can be baffling if you don't know why.

Fix

user is await fetchUser()
say user.name   -- works! "Alice"
Rule of thumb: If a function uses await inside it, you almost certainly need to await when calling it too. Quill will warn you about this with quill check.

8. Modifying a list while looping over it

Adding or removing items from a list while you're iterating through it leads to unexpected behavior.

Wrong

numbers are [1, 2, 3, 4, 5]

for each num in numbers:
  if num is 3:
    numbers.remove(num)   -- modifying the list mid-loop!

say numbers
-- Expected: [1, 2, 4, 5]
-- Actual:   [1, 2, 4] ... it skipped 4!

Why it's wrong: When you remove an item, the loop's internal index gets out of sync with the list's new length. Items get skipped or the loop ends early. This is a classic bug in every language.

Fix

numbers are [1, 2, 3, 4, 5]

-- Build a new list instead of modifying the original:
filtered is numbers.filter(to (num): num is not 3)

say filtered
-- [1, 2, 4, 5]

9. String concatenation with numbers

Trying to glue a string and a number together with + can produce unexpected results.

Wrong

count is 5
say "Count: " + count
Error [TYPE_MISMATCH] in app.quill:2

  2 |  say "Count: " + count
                     ^
  Cannot use '+' with String and Number.

  Hint: Use string interpolation instead:
        say "Count: {count}"

Why it's wrong: Quill doesn't silently convert types during concatenation (unlike JavaScript). This prevents bugs where "5" + 3 accidentally gives "53". Use string interpolation instead — it's safer and easier to read.

Fix

count is 5
say "Count: {count}"

10. Using my outside a class

The my keyword refers to the current instance and only works inside describe blocks (Quill's classes).

Wrong

-- At the top level of a file:
my.name is "Alice"
Error [CONTEXT] in app.quill:1

  1 |  my.name is "Alice"
       ^^
  'my' can only be used inside a 'describe' block.
  It refers to the current instance of a class.

  Hint: If you want a plain variable, just write:
        name is "Alice"

Why it's wrong: my is Quill's equivalent of this or self in other languages. It only makes sense inside a class definition where there is an "instance" to refer to.

Fix

-- For a plain variable:
name is "Alice"

-- Or inside a class:
describe Person:
  to setup name:
    my.name is name   -- correct: inside a describe block

11. Forgetting new for class instances

When you create an instance of a class, you need the new keyword.

Wrong

describe Dog:
  to setup name:
    my.name is name

-- Missing 'new':
buddy is Dog("Buddy")
Error [CALL] in app.quill:6

  6 |  buddy is Dog("Buddy")
                ^^^
  'Dog' is a class, not a function. Use 'new' to
  create an instance.

  Hint: Write:  buddy is new Dog("Buddy")

Why it's wrong: Without new, Quill tries to call Dog as a regular function, which fails because classes need to be instantiated. The new keyword tells Quill to create a fresh instance and run the setup method.

Fix

buddy is new Dog("Buddy")
say buddy.name   -- "Buddy"

12. Circular imports

File A imports file B, and file B imports file A. This creates an infinite loop.

Wrong

-- fileA.quill
bring "./fileB"

to helperA:
  say "A"
-- fileB.quill
bring "./fileA"   -- circular!

to helperB:
  say "B"
Error [CIRCULAR_IMPORT] in fileB.quill:1

  Circular dependency detected:
    fileA.quill -> fileB.quill -> fileA.quill

  Hint: Move shared code into a third file that both
        can import, or restructure to remove the cycle.

Why it's wrong: When A loads B, B tries to load A, which tries to load B again, forever. Quill detects this and stops with an error.

Fix

-- shared.quill (new file with shared code)
to sharedHelper:
  say "shared"

-- fileA.quill
bring "./shared"

-- fileB.quill
bring "./shared"

13. Off-by-one errors with range

Quill's range function is exclusive of the end value, which surprises many developers.

Wrong (if you expect [1,2,3,4,5])

numbers is range(1, 5)
say numbers
-- You expected: [1, 2, 3, 4, 5]
-- You got:      [1, 2, 3, 4]

What happens: No error — the code runs fine, but the result doesn't include 5. This is a logic bug. range(1, 5) produces numbers from 1 up to (but not including) 5.

Fix

-- To include 5, go one higher:
numbers is range(1, 6)
say numbers
-- [1, 2, 3, 4, 5]
Why exclusive? This convention (used by Python, Rust, and many others) prevents a whole class of fencepost bugs. range(0, list.length) gives you every valid index without needing to subtract 1.

14. Async in loops

Using await inside a for each loop runs requests one at a time (sequentially), which might not be what you want.

Slow (sequential, one by one)

urls are ["https://api.com/1", "https://api.com/2", "https://api.com/3"]

for each url in urls:
  response is await fetch(url)   -- waits for each one before starting the next
  say response.status

What happens: Each request waits for the previous one to finish before starting. If each takes 1 second, three requests take 3 seconds total. This is correct but slow.

Fix (parallel, all at once)

urls are ["https://api.com/1", "https://api.com/2", "https://api.com/3"]

-- Fire all requests at the same time:
promises are urls.map(to (url): fetch(url))
responses is await Promise.all(promises)

for each response in responses:
  say response.status
When is sequential correct? Sometimes you genuinely need each request to finish before the next one starts (e.g., each request depends on the previous response). In that case, await inside the loop is the right approach.

15. Mutable state in components

Modifying state directly instead of using state setters means your UI won't update.

Wrong

component Counter:
  count is state(0)

  to increment:
    count is count + 1   -- directly modifying! UI won't update

  to render:
    give back <button on click is increment>
      "{count}"
    </button>

What happens: The variable count changes internally, but the component doesn't re-render because Quill's reactivity system doesn't know the state changed. You must use the setter function so Quill can track the update.

Fix

component Counter:
  [count, setCount] is state(0)

  to increment:
    setCount(count + 1)   -- use the setter! UI will update

  to render:
    give back <button on click is increment>
      "{count}"
    </button>
Tip: If you've used React, this pattern will feel familiar. The destructured [value, setter] pattern is how Quill tracks what needs to re-render.

16. Forgetting to handle nothing

A function that doesn't always give back a value will return nothing (Quill's version of null/undefined), which can cause errors downstream.

Wrong

to findUser id:
  for each user in users:
    if user.id is id:
      give back user
  -- what if the user isn't found? gives back nothing!

result is findUser(999)
say result.name   -- Error: Cannot read 'name' of nothing

Why it's wrong: If no user matches the ID, the function ends without a give back statement, so it returns nothing. Then trying to access .name on nothing crashes.

Fix

to findUser id:
  for each user in users:
    if user.id is id:
      give back user
  give back nothing   -- explicit!

result is findUser(999)
if result is not nothing:
  say result.name
otherwise:
  say "User not found"

17. Shadowing a variable by accident

Declaring a variable inside a block with the same name as one outside it. The inner variable "shadows" the outer one, and changes to it don't affect the outer variable.

Wrong

total is 0

to addToTotal amount:
  total is amount   -- this creates a NEW local 'total', doesn't modify the outer one!

addToTotal(10)
addToTotal(20)
say total   -- still 0!

What happens: No error is thrown. The total inside addToTotal is a brand new local variable that disappears when the function ends. The outer total is never touched. This is a silent logic bug.

Fix

total is 0

to addToTotal amount:
  change total to total + amount   -- explicitly modify the outer variable

addToTotal(10)
addToTotal(20)
say total   -- 30
Why this happens: Quill treats x is value inside a function as a new local declaration by default. To modify a variable from an outer scope, use change x to value. This design prevents accidental mutation of global state.

18. Passing the wrong number of arguments

Calling a function with too many or too few arguments.

Wrong

to greet firstName, lastName:
  say "Hello, {firstName} {lastName}!"

-- Too few arguments:
greet("Alice")
Error [ARGS] in app.quill:5

  5 |  greet("Alice")
       ^^^^^
  'greet' expects 2 arguments (firstName, lastName)
  but was called with 1.

  Hint: Add the missing argument:
        greet("Alice", "Smith")

Why it's wrong: Quill functions are strict about the number of arguments. Unlike JavaScript, missing arguments don't silently become undefined — Quill catches this at compile time.

Fix

greet("Alice", "Smith")

-- Or make the parameter optional with a default value:
to greet firstName, lastName is "":
  if lastName is "":
    say "Hello, {firstName}!"
  otherwise:
    say "Hello, {firstName} {lastName}!"

19. Using otherwise if after otherwise

Putting an otherwise if branch after the catch-all otherwise block.

Wrong

if score > 90:
  grade is "A"
otherwise:
  grade is "F"
otherwise if score > 80:   -- this will never run!
  grade is "B"
Error [SYNTAX] in grades.quill:5

  5 |  otherwise if score > 80:
       ^^^^^^^^^
  'otherwise if' cannot appear after 'otherwise'.
  The 'otherwise' block must be last.

  Hint: Move 'otherwise if' branches before 'otherwise'.

Why it's wrong: The otherwise block is the catch-all default — it must come last. Any branches after it would be unreachable. Quill catches this mistake at compile time.

Fix

if score > 90:
  grade is "A"
otherwise if score > 80:
  grade is "B"
otherwise:
  grade is "F"

Debugging Strategies

Beyond the debugger and error messages, here are proven strategies for tracking down bugs efficiently.

Print debugging with say

The simplest and often most effective technique. Drop say statements into your code to trace what values variables have at different points.

to processOrder items:
  say "[DEBUG] processOrder called with {items.length} items"
  total is 0
  for each item in items:
    say "[DEBUG] item: {item.name}, price: {item.price}"
    total is total + item.price
  say "[DEBUG] total before tax: {total}"
  give back total * 1.08
Tip: Prefix your debug messages with [DEBUG] so you can easily find and remove them later. Some developers use say! (if available) for debug-only output that gets stripped in production builds.

Divide and conquer

When you have a big program and something is wrong, comment out half the code. If the bug disappears, it's in the half you commented out. If the bug remains, it's in the other half. Keep halving until you find it.

-- Comment out sections to isolate the bug:

-- step1()
-- step2()
step3()        -- bug appears? it's in step3
-- step4()

This is especially useful for logic bugs that don't produce error messages.

Rubber duck debugging

Explain your code out loud, line by line, to anyone (or anything — a rubber duck works). The act of explaining often reveals the mistake because you have to think about what each line actually does, not what you assume it does.

How it works: You say "On this line, I set total to zero. Then I loop through each item... wait. I'm adding to total but I declared a new total inside the loop! That's the bug!" The act of verbalizing forces you to think carefully.

Reading the compiled JavaScript

Since Quill compiles to JavaScript, you can inspect the output to understand what's actually running:

# Generate the JS output
quill build app.quill

# Read it
cat app.js

This is particularly helpful for understanding how Quill's syntactic sugar translates to actual operations. If the compiled JS looks wrong, you've found a compiler bug (rare, but possible) or a misunderstanding of Quill syntax.

Using the type checker

Quill's check command analyzes your code without running it and catches many errors before they become runtime bugs:

quill check app.quill
app.quill:14  Warning: 'result' might be nothing here.
                        Add a check before accessing .name
app.quill:22  Error:   Cannot call 'push' on a String.
                        Did you mean to use a List?

Found 1 error and 1 warning.

Run quill check early and often. Many developers add it to their editor's save hook so every file is checked automatically.

Using tests to prevent regressions

Once you've fixed a bug, write a test that would have caught it. This prevents the same bug from coming back.

-- test_average.quill
bring "./average"

test "average of three numbers":
  result is average([80, 90, 100])
  expect result to be 90

test "average of empty list":
  result is average([])
  expect result to be 0

test "average of one number":
  result is average([42])
  expect result to be 42
quill test test_average.quill

See the Testing docs for more on writing and running tests.

Check your assumptions

Most bugs come from wrong assumptions. Before diving deep, verify the basics:

Minimal reproducible example

When a bug is hard to find, try to create the smallest possible program that still shows the problem. Start with your full program and remove things one by one until you have the bare minimum that triggers the bug.

-- Start: 200-line program with a bug
-- Step 1: Remove unrelated functions  (150 lines)
-- Step 2: Hardcode inputs              (80 lines)
-- Step 3: Remove UI code               (30 lines)
-- Step 4: Simplify the data            (10 lines)
-- Now you can see the bug clearly!
Why this works: Each thing you remove either makes the bug disappear (meaning the thing you removed was involved) or doesn't (meaning you can safely ignore it). By the end, you have a tiny program where the bug is obvious.

Compare with working code

If something used to work and now it doesn't, use version control to compare:

# See what changed since the last working version
git diff HEAD~3 app.quill

# Or check out the old version temporarily
git stash
quill run app.quill   -- does the old version work?
git stash pop

Often the bug is in something you changed recently. Narrowing down which change broke things is half the battle.

Getting Help

Stuck on something? Here's where to look, in order:

1. Check the error message hints

Quill's error messages almost always include a hint. Read it carefully — it often tells you exactly what to do.

2. Search the docs

The documentation pages cover every part of the language. The Language Reference is especially useful when you're unsure about syntax, and the Standard Library docs list every built-in function.

3. Try the playground

The online playground lets you test small code snippets instantly without setting anything up. It's a great way to verify how a feature works in isolation.

4. Ask on Discord

The Quill community on Discord is active and welcoming. Post your question in the #help channel with:

5. Check the examples directory

The examples directory contains dozens of working programs. If you're trying to do something specific, there's probably an example that shows how. Seeing working code is often more helpful than reading explanations.

Remember: Every experienced Quill developer was once confused by these same issues. Don't hesitate to ask for help — the community is here to support you.

Next steps