website is under construction
Language

Error Handling

Ghost has no exceptions. There is no try, no catch, and no throw. Every failure — a syntax error, a type mismatch, a bad argument, a missing file — is an ordinary value that travels back up the call chain until it reaches the top, where it is reported and the program stops.

That is a deliberate design decision rather than a missing feature: an error in Ghost is data, not control flow.

What a failure looks like

Every failure Ghost reports carries the same five things: what sort of mistake it is, a sentence saying what happened, the exact place it happened with the offending text underlined, what was in flight at the time, and — where there is one — what to do about it.

name error: `mesage` is not defined
 --> example.gs:4:22
  |
4 |   return "Hello, " + mesage
  |                      ^^^^^^
  |
  = in greet(), called at example.gs:7:1
  = help: did you mean `message`?

Reading it top to bottom:

  • name error — the kind of mistake, named below. Ghost never reports a generic "runtime error".
  • example.gs:4:22 — the file, line, and column.
  • The quoted line — the source itself, with ^ under exactly the text that failed.
  • = in greet(), called at ... — the call trace, recorded as the error unwinds. Deep traces are capped at eight frames, with a count of the rest.
  • = help: — what to do next, rather than a restatement of the problem.

Not every failure has all of these. A trace only appears when the error happened inside a call, and a help line only when there is something useful to say.

Kinds of error

The first two words say what sort of mistake it is before the sentence explains it.

KindWhat it means
syntax errorsource that could not be read
name errora reference to something that was never defined
type erroran operation applied to the wrong sort of value
argument errora call whose arguments do not fit what it is calling
index errora subscript outside what it indexes
value errora value of the right type the operation cannot accept
property errora member the value does not have
import errora module that could not be found, read, or resolved
system errorthe world outside the program refusing — a missing file, a port that will not bind
internal errora bug in Ghost itself, with a note asking for it to be reported

Syntax errors

Syntax errors are found while reading your code, before any of it runs:

1 + * 2

Parsing reports every syntax error it finds rather than stopping at the first, so one pass over a file tells you about all of them. If a file has any, none of it runs.

Name errors, and "did you mean?"

A name that was never defined is only noticed when execution reaches the line that reads it:

console.log("this line runs")
console.log(mesage)
name error: `mesage` is not defined
 --> example.gs:2:13
  |
2 | console.log(mesage)
  |             ^^^^^^
  |
  = help: did you mean `message`?

The suggestion is a real edit-distance search over the names actually in scope — variables, class members, or a module's own methods, depending on where the mistake is — not a guess. The same machinery covers misspelled properties and misspelled imports:

property error: module `math` has no property `pii`
 --> example.gs:2:18
  |
2 | console.log(math.pii)
  |                  ^^^
  |
  = help: did you mean `math.pi`?

Reaching for a standard library module you have not imported is a name error too, and the help names the exact import to add:

name error: `math` is not defined
 --> example.gs:1:1
  |
1 | math.sqrt(16)
  | ^^^^
  |
  = help: `math` is not a global — import it: `import "ghost:math"`

Type errors

Ghost does not convert values for you:

console.log("Ghost " + 1)
type error: cannot use `+` between string and number
 --> example.gs:1:22
  |
1 | console.log("Ghost " + 1)
  |                      ^

Use a template literal to build a string out of mixed types, or call toString() on the number.

count = 1

console.log(`Ghost ${count}`)

Argument errors

Every library method checks its own arity and argument types, so a miscall is reported as a call rather than as a crash somewhere inside it:

argument error: `math.sqrt()` expects 1 argument, got 0
 --> example.gs:2:6
  |
2 | math.sqrt()
  |      ^^^^

Functions and methods you define yourself are checked for a minimum but have no maximum, so the wording differs — expects at least 2 arguments, got 1. Either way the error is raised before the body runs, so no frame for the call appears in the trace — it never started.

Nothing can crash the host

A Ghost program cannot take down the Go program running it. Runaway recursion is counted and reported as an ordinary error at 4,096 call frames rather than overflowing the stack, and a bug in Ghost itself is caught at the boundary and reported as an internal error asking you to file it. Set GHOST_DEBUG in the environment to attach the Go stack trace to one of those.

Raising an error yourself

ghost.abort() is the one place a script deliberately raises an error rather than an operation failing on its own:

import "ghost:ghost"

ghost.abort("Something bad happened")

It takes exactly one argument, and that argument must be a string or null. A string raises a value error carrying that message; null does nothing at all, which lets a check with nothing to complain about hand back null and be aborted on unconditionally.

Values that aren't errors

Not every miss is an error. A few operations answer with null instead, and are worth knowing so you don't reach for error handling where a comparison will do:

  • A list index that is out of range.
  • A map key that isn't present.
  • A string index that is out of range.
  • first(), last(), pop(), and shift() on an empty list.
  • A function that ends without a return.
list = [1, 2, 3]

if (list[10] == null) {
  console.log("nothing there")
}

The rule is roughly: a read that names a position is lenient, and an operation that names a range validates it. slice() raises an index error for bounds outside the list, where list[10] answers null.