Errors

Catch, throw and clean up with TRY, CATCH and FINALLY, decide what a failing command does with ErrorMode, and read the errors the editor reports.

In one line#

A program meets two kinds of errors. The compiler finds the first kind before anything runs. The second kind happens while the game runs, and stops it unless you catch it with TRY … CATCH. Catalogue commands have one more option: ErrorMode decides whether their failures stop, report or stay silent.

Two kinds of errors#

Compile errorRuntime error
Whenbefore the program startswhile it runs
Examplesa misspelled command, ENDIF without IF, RETHROW outside a CATCHa command that fails, THROW, a DIV 0, a released handle
What happensevery error is listed, and nothing runsthe program stops at that line, unless a TRY catches it
Can you catch itno, fix the sourceyes, with TRY / CATCH

What the console shows#

Both kinds go to the editor's console, on three lines at most:

BASIC · Compilation · <file> · Line <n>, column 1
SyntaxError: <message>

BASIC · Execution · <file> · Line <n>, column <c>
<name>: <message>
Command: <the failing call>
  • The file is the script, or the module where the line lives when the program is split with #INCLUDE. See The preprocessor.
  • The Command: line appears when a catalogue command failed. It shows the call with the values it received, for example MemblockByte(999, 0).
  • Double-click an error line to open the script at that line.

A command failure looks like this:

RangeError: BASIC MemblockByte(999, 0) failed: Classic BASIC memory block 999 does not exist. Create or load the referenced resource before running this command.
Command: MemblockByte(999, 0)

⚠️ A JavaScript message in the Compilation phase means a compiler gap. "Unexpected identifier", "missing ) after argument list", "await is only valid in async functions" or "Identifier … has already been declared" come from a form the compiler accepted when it should have refused it. The chapters mark those forms with ⚠️, and give the spelling that works.

TRY, CATCH, FINALLY#

TRY
  <statements>
[CATCH [<name>[$]]
  <statements>]
[FINALLY
  <statements>]
ENDTRY
TRY
  THROW "out of ammo"
CATCH problem$
  Console("caught: " + problem$)
FINALLY
  Console("cleanup")
ENDTRY
Console("after: " + problem$)

The program prints caught: out of ammo, then cleanup, then after: out of ammo.

  • CATCH runs only if something in the TRY part failed.
  • FINALLY always runs: after a normal end, after an error, after EXITFUNCTION or RETURN, after END and after a GOTO out of the block.
  • CATCH and FINALLY are each optional, but a TRY needs at least one: "TRY requires CATCH or FINALLY".
  • The order is fixed: TRY, then CATCH, then FINALLY. One CATCH at most: there is no CATCH per error type. Inside the CATCH, test the error's name or code instead.
  • ENDTRY and END TRY are the same.
  • A TRY works anywhere: in the main program, in the frame loop, in a FUNCTION, in a lifecycle function such as update.

The compile errors of this block:

MessageCause
CATCH without TRY, FINALLY without TRY, ENDTRY without TRYthe opening TRY is missing
Duplicate CATCH in TRY, Duplicate FINALLY in TRYa second clause of the same kind
CATCH cannot follow FINALLYwrong order
TRY requires ENDTRYthe block is never closed

The catch variable#

The name after CATCH receives the error. Its sigil decides what you get.

You writeThe variable holds
CATCH problem$the message, as a string
CATCH problema read-only record with four fields
CATCHnothing: the error is caught and dropped
FieldContent
problem.namethe kind of error: Error for a THROW, RangeError, TypeError… for the others
problem.messagethe message
problem.codethe code given to THROW, or ""
problem.commandthe failing command call, such as MemblockByte(999, 0), or ""
TRY
  value = MemblockByte(999, 0)
CATCH failure
  Console(failure.name)      ' RangeError
  Console(failure.command)   ' MemblockByte(999, 0)
  Console(failure.message)   ' BASIC MemblockByte(999, 0) failed: Classic BASIC memory block 999 does not exist. …
ENDTRY

The variable stays readable after ENDTRY, as the first example shows. Inside a FUNCTION, it belongs to the function.

⚠️ Give the catch variable a name of its own. If the same name is declared or assigned anywhere else in the same scope, before or after the TRY, the compiler accepts it but the program cannot start ("Identifier … has already been declared"). To keep the message, copy it into another variable: report$ = "last error: " + problem$.

OPTION EXPLICIT
GLOBAL message$ = ""
TRY
  THROW "x"
CATCH message$
ENDTRY

THROW#

THROW <value>

THROW raises an error that a CATCH can receive. What you throw decides the fields:

You throwmessagecode
a string: THROW "save failed"the string""
a number: THROW 404the number as text, 404""
a record with a message field, and optionally a code fieldthe field messagethe field code
TYPE Failure
  message AS string
  code AS integer
ENDTYPE

f AS Failure
f.message = "save failed"
f.code = 7

TRY
  THROW f
CATCH err
  Console(err.message + " / " + Str(err.code))   ' save failed / 7
ENDTRY
  • THROW without a value is a compile error: "THROW requires a value".
  • A thrown error leaves every function on the way until a CATCH takes it, so a function can throw and its caller can catch.
  • name is always Error for a THROW.
OPTION EXPLICIT
GLOBAL attempts AS integer = 0
GLOBAL connected AS integer = 0

FUNCTION Connect()
  attempts = attempts + 1
  IF attempts < 3 THEN THROW "timeout"
ENDFUNCTION

WHILE connected = 0
  TRY
    Connect()
    connected = 1
  CATCH problem$
    Console("attempt " + Str(attempts) + ": " + problem$)
  ENDTRY
ENDWHILE
Console("connected after " + Str(attempts))   ' connected after 3

⚠️ Do not THROW the error you caught. THROW failure builds a new error from its message and code: the name becomes Error and the command is lost. To pass the same error on, use RETHROW.

RETHROW and nested blocks#

RETHROW

RETHROW sends the error being handled on to the next TRY outside, unchanged.

TRY
  TRY
    THROW "disk full"
  CATCH inner$
    Console("inner: " + inner$)
    RETHROW
  FINALLY
    Console("inner finally")
  ENDTRY
CATCH outer
  Console("outer: " + outer.message)
ENDTRY

The program prints inner: disk full, then inner finally, then outer: disk full. The inner FINALLY runs before the outer CATCH.

  • RETHROW is only allowed inside a CATCH (or inside a block nested in a CATCH). Anywhere else: "RETHROW requires an active CATCH block".
  • It takes no value: "RETHROW does not accept a value".
  • TRY blocks nest to any depth. An error goes to the nearest enclosing CATCH.

END and GOTO pass through#

END and GOTO are not errors, and a CATCH never sees them. FINALLY still runs on the way out.

TRY
  Console("before")
  END
CATCH e
  Console("caught END")
FINALLY
  Console("finally runs")
ENDTRY
Console("never")

The program prints before, then finally runs, and stops. A GOTO to a label outside the block behaves the same: FINALLY runs, then the jump happens.

When a command fails: ErrorMode#

A catalogue command can fail: a missing file, an id that does not exist, a value out of range. ErrorMode decides what happens then.

ErrorMode(<mode>)
ErrorMode()
ModeValueWhat a failing command does
#AK_ERROR_STOP2, the defaultstops the program, or goes to the CATCH of the enclosing TRY
#AK_ERROR_REPORT1writes the error to the console, returns a neutral value, and the program goes on
#AK_ERROR_IGNORE0returns a neutral value in silence, and the program goes on

The neutral value is 0 for a command that returns a number, "" for one that returns a string, nothing otherwise.

ErrorMode(#AK_ERROR_IGNORE)
value = MemblockByte(999, 0)
Console(Str(value))                                    ' 0
IF ErrorOccurred() THEN Console("failed: " + LastError())   ' failed: Classic BASIC memory block 999 does not exist
Console(Str(ErrorMode()))                              ' 0
  • ErrorMode() without an argument returns the current mode.
  • The mode stays in force until the next ErrorMode(...).
  • Any other value is an error: "Classic BASIC error mode must be #AK_ERROR_IGNORE, #AK_ERROR_REPORT, or #AK_ERROR_STOP".

ErrorMode covers catalogue commands, and nothing else. A THROW, a division by zero with DIV, a released handle or a command called with the wrong number of arguments still raise an error in every mode.

ErrorMode(#AK_ERROR_IGNORE)
a = 1
b = 0
TRY
  c = a DIV b
CATCH e$
  Console("caught: " + e$)   ' caught: BASIC integer division by zero
ENDTRY

A few commands finish their work after they return: the share sheet, the file chooser, opening a tab. If they fail, it is too late to stop the program. They set LastError and ErrorOccurred, and write to the console unless the mode is #AK_ERROR_IGNORE.

LastError and ErrorOccurred#

CommandReturns
LastError()the message of the last command failure, without the BASIC …() failed: prefix and the hint. It stays until the next failure.
ErrorOccurred()1 if a command failed since the last call, then resets to 0

Both are set in every mode, #AK_ERROR_STOP included. Inside a CATCH you can read them too.

TRY
  value = MemblockByte(999, 0)
CATCH
ENDTRY
Console(Str(ErrorOccurred()))   ' 1
Console(Str(ErrorOccurred()))   ' 0
Console(LastError())            ' Classic BASIC memory block 999 does not exist

⚠️ Read ErrorOccurred() once. The second call already answers 0. Store the result if you need it twice.

Runtime errors the language raises#

Besides commands and THROW, a few operations raise an error by themselves:

OperationMessage
a DIV 0BASIC integer division by zero
storing 1 / 0 in an AS integer variableBASIC integer requires a finite value
FOR i = 1 TO 5 STEP 0BASIC FOR STEP cannot be zero
GetRef or SetRef on a released handleBASIC reference handle has been released
a command called with a number of arguments it does not acceptBASIC classique command <name> received <n> argument(s); expected …

1 / 0 on an untyped variable does not raise an error: it gives Infinity.

Where an uncaught error goes#

ProgramWhat an uncaught runtime error does
a straight program, or a DO … LOOPstops the program at that line
a lifecycle function (update, onCollisionEnter…)reports the error. The Engine calls the function again on the next frame or event, so the error repeats

See Functions for the lifecycle functions, and The game loop and tasks for errors inside a task.

Compared with classic dialects#

DialectWhat it hasIn aukimi
AGKSetErrorMode with 0, 1, 2; GetLastError, GetErrorOccurredsame modes and values, without the verb: ErrorMode(...), LastError(), ErrorOccurred()
DarkBASIC Prono structured errorsTRY / CATCH / FINALLY
PureBasicOnErrorCall, OnErrorGotoTRY / CATCH, one handler per block instead of one per program
Visual BasicOn Error Resume Next, On Error GoToErrorMode(#AK_ERROR_IGNORE) for commands, TRY for the rest