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.

Game Development with Godot & GDScript  Module 10 – Static typing, debugging and testing

Errors, asserts and defensive code

What a GDScript runtime error stops and what carries on, how to check Error codes, when assert and push_error fit, and how to check input at the edges.

  • Intermediate
  • 20 minutes
  • Examples run with godot 4.7.2-stable and Python 3.14.8
  • By MySmartCoPilot
Free core, Pro depth: 7 of 20 minutes free
Free: A runtime error stops one function, and the caller carries on · Engine functions return an Error code instead of stopping · Report, refuse or return: four tools and where each belongs. With a pass: 3 in-depth parts and 3 interview answers, about 13 minutes.

What you will learn

  • Explain what a GDScript runtime error stops and what value the caller receives
  • Check the Error codes of engine functions and turn them into messages with error_string()
  • Choose between assert(), push_error(), push_warning() and a returned Error for a problem
  • Validate data where it enters the game, so that a malformed file cannot crash it or half-apply

Before you start

What you’ll learn · Before you start

What you will learn

  • Explain what a GDScript runtime error stops and what value the caller receives
  • Check the Error codes of engine functions and turn them into messages with error_string()
  • Choose between assert(), push_error(), push_warning() and a returned Error for a problem
  • Validate data where it enters the game, so that a malformed file cannot crash it or half-apply

Before you start

On this page

A damage function met a null target, Godot printed an error, and the fight went on with every hit dealing 0. GDScript errors are not exceptions: a runtime error ends the one function it happens in, and the caller carries on with a default value. So a game guards itself in three ways. It checks the Error codes that engine functions return. It states its own assumptions with asserts, which stop a function in debug runs. And it checks every file and value that comes from outside, which is called defensive code. Each is run below on Godot 4.7.2.

A runtime error stops one function, and the caller carries on

stops_one_function.gd has four small functions, and each meets a different runtime error: A mistake found while a line runs, such as calling a method on null or reading past the end of an array. GDScript stops the function it happens in, prints SCRIPT ERROR with a backtrace and lets the caller go on.: a method called on null, an array index past the end, an integer division by zero and a missing entry of a PackedStringArray. _init() calls all four and prints what each returned:

stops_one_function.gd: four functions, four runtime errors GD · errors/stops_one_function.gd
extends SceneTree


func damage_to(target: Node) -> int:
	print("  damage_to() starts")
	var armour: int = target.get_meta("armour")
	print("  damage_to() ends")
	return 10 - armour


func third_wave(waves: Array[int]) -> int:
	return waves[2]


func gold_each(gold: int, players: int) -> int:
	@warning_ignore("integer_division")
	return gold / players


func room_title(room: int) -> String:
	var titles: PackedStringArray = ["Cellar", "Crypt"]
	return titles[room]


func _init() -> void:
	print("damage: ", damage_to(null))
	print("third wave: ", third_wave([4, 6]))
	print("gold each: ", gold_each(90, 0))
	print("room title: '", room_title(3), "'")
	print("the caller is still running")
	quit()
Four runtime errors, then the Error codes of four engine calls, Godot 4.7.2 Python · errors_session.py
from session import files, run

# Four runtime errors in four functions, each called by _init(), then the Error codes of three engine functions that
# fail and one that works.
files("project.godot", "errors/stops_one_function.gd=stops_one_function.gd", "codes/codes.gd=codes.gd")
run("""
!godot --headless --no-header --script stops_one_function.gd
godot --headless --no-header --script codes.gd
""")

Output

$ godot --headless --no-header --script stops_one_function.gd
  damage_to() starts
SCRIPT ERROR: Cannot call method 'get_meta' on a null value.
          at: damage_to (res://stops_one_function.gd:6)
          GDScript backtrace (most recent call first):
              [0] damage_to (res://stops_one_function.gd:6)
              [1] _init (res://stops_one_function.gd:26)
damage: 0
SCRIPT ERROR: Out of bounds get index '2' (on base: 'Array[int]')
          at: third_wave (res://stops_one_function.gd:12)
          GDScript backtrace (most recent call first):
              [0] third_wave (res://stops_one_function.gd:12)
              [1] _init (res://stops_one_function.gd:27)
third wave: 0
SCRIPT ERROR: Division by zero error in operator '/'.
          at: gold_each (res://stops_one_function.gd:17)
          GDScript backtrace (most recent call first):
              [0] gold_each (res://stops_one_function.gd:17)
              [1] _init (res://stops_one_function.gd:28)
gold each: 0
SCRIPT ERROR: Out of bounds get index '3' (on base: 'PackedStringArray')
          at: room_title (res://stops_one_function.gd:22)
          GDScript backtrace (most recent call first):
              [0] room_title (res://stops_one_function.gd:22)
              [1] _init (res://stops_one_function.gd:29)
room title: ''
the caller is still running
$ godot --headless --no-header --script codes.gd
FileAccess.open(): file is null: true, error 7, File not found
ConfigFile.load(): error 7, File not found
JSON.parse(): error 43, Parse error: Expected value, got 'EOF'
DirAccess.make_dir_recursive_absolute(): error 0, OK

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 errors_session.py

Step through the first call, the one with the null target:

Interactive · message by message One runtime error, one function stopped Step through the call of damage_to(null). Each step says who acts and what the run printed.
One runtime error, one function stopped, step 6 of 6. Messages so far: _init() to damage_to(): damage_to(null); damage_to() to Godot: SCRIPT ERROR; damage_to() to _init(): returns 0._init()printed damage: 0damage_to()line 6: null.get_meta()Godotruns the scriptdamage_to(null)SCRIPT ERRORreturns 0

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. The call. _init() calls damage_to(null), with no target at all. damage_to() starts and prints its first line.
  2. The bad line. Line 6 calls get_meta() on target, which is null. There is no object to call it on, so the line cannot run.
  3. The error. Godot prints SCRIPT ERROR: Cannot call method get_meta on a null value, with a backtrace of two frames: damage_to() at line 6, called by _init() at line 26.
  4. The function stops. damage_to() stops at once and never prints its last line. It returns 0, the default value of its return type, int, as if 0 were the damage.
  5. The caller goes on. _init() prints damage: 0 and calls the next function. Three more errors went the same way, and the run ended with the caller is still running and exit status 0.
  6. With a debugger. With a debugger attached, the editor's or the one that -d starts, the same error also pauses the game on line 6, so you can look at target before anything goes on.

Every function stopped at its bad line and returned the default value of its return type: 0 for the three that return an int, an empty String for room_title(). Nothing told the caller that the value was not real, so damage: 0 looked like an answer. All four are runtime errors, the GDScript errors that show only while the game runs. The backtrace: The list of function calls that led to an error or a warning, the most recent first, each with its file and line. Godot prints one under script errors and warnings since version 4.5. More in “Your first script and the game loop” under each one names the line and the caller, which is the fastest way to the cause.

Checkpoint

Check: What a runtime error stops

  1. Question 1 of 2 room_title(room: int) -> String reads titles[room] from a PackedStringArray of two titles. What does print("room title: '", room_title(3), "'") print after the error?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: room title: ''

    A runtime error ends the function at its bad line, and the function hands back the default value of its return type: 0 for an int, "" for a String.

  2. Question 2 of 2 A function deal(target: Node, amount: int) -> void calls target.take_hit(amount) on its first line and then adds amount to a damage counter. target is null. What happens?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: deal() stops at its first line, the counter is never added to, and the caller goes on

    Work that comes after the bad line is lost without a word, which is why a value check belongs before the line that can fail.

Engine functions return an Error code instead of stopping

Many engine functions report failure with a value of the Error enum, an error code: A value of Godot's Error enum that many engine functions return instead of stopping: OK is 0, and every failure is a positive number, such as 7 for ERR_FILE_NOT_FOUND. error_string() turns a code into words., and leave the decision to you. OK is 0 and every other code is a positive number, so if err != OK: is the usual test, and error_string() turns a code into words. The second half of the session made four engine calls: two read files that do not exist, one parses a piece of JSON that is cut off, and one makes a folder:

codes.gd: four engine calls and the Error each returned GD · codes/codes.gd
extends SceneTree


func _init() -> void:
	var file := FileAccess.open("user://settings.json", FileAccess.READ)
	var err := FileAccess.get_open_error()
	print("FileAccess.open(): file is null: %s, error %d, %s" % [file == null, err, error_string(err)])

	var config := ConfigFile.new()
	err = config.load("user://controls.cfg")
	print("ConfigFile.load(): error %d, %s" % [err, error_string(err)])

	var json := JSON.new()
	err = json.parse('{"volume": 0.8, "difficulty": ')
	print("JSON.parse(): error %d, %s: %s" % [err, error_string(err), json.get_error_message()])

	err = DirAccess.make_dir_recursive_absolute("user://saves/slot_1")
	print("DirAccess.make_dir_recursive_absolute(): error %d, %s" % [err, error_string(err)])
	if err != OK:
		push_error("could not make the save folder: " + error_string(err))
	quit()

What each call returned, in the run above:

Call What came back
FileAccess.open() null; FileAccess.get_open_error() gave 7, File not found
ConfigFile.load() 7, File not found
JSON.parse() on cut-off text 43, Parse error, with the message Expected value, got ‘EOF’
DirAccess.make_dir_recursive_absolute() 0, OK: the folder was made

None of these calls stopped the script. A function that ignores the returned code carries on as if the file had loaded, and the trouble shows up later, far from its cause.

Using what an engine call returned without checking it

var file := FileAccess.open(path, FileAccess.READ) followed at once by file.get_as_text() stops with a runtime error when the file is missing, because file is null. Test if file == null: first and report error_string(FileAccess.get_open_error()).

Checkpoint

Check: Error codes from engine functions

  1. Question 1 of 2 err holds the Error a function returned. What does if err: test?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Whether something failed, because OK is 0 and every other code is positive

    if err != OK: says the same thing more plainly.

  2. Question 2 of 2 FileAccess.open("user://settings.json", FileAccess.READ) is called and the file does not exist. What comes back, and how do you get the reason?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: null; FileAccess.get_open_error() gives 7, which error_string() turns into File not found

    Test the file for null before using it, and report error_string(FileAccess.get_open_error()).

Report, refuse or return: four tools and where each belongs

GDScript gives four ways to deal with a problem you can foresee, and they differ in whether the function goes on:

Tool What it does
assert(condition, message) checks an assumption of your own code; when it is false, an error and the function returns its default
push_error(message) prints an error with a backtrace, and the function goes on
push_warning(message) the same, as a warning
return ERR_INVALID_DATA hands the problem to the caller, who decides what the game does

The @GDScript reference calls assert() a stronger form of push_error(), and adds that the code inside it runs only in debug builds and editor runs. Asserts are therefore for mistakes in your own code, never for a damaged file from a player. Defensive code means checking data where it enters the game: a settings file, a save, a network message or a value typed into the inspector. Inside the game, between your own typed functions, the types of the previous lesson already check most of it.

Checkpoint

Check: assert, push_error, push_warning or an Error

  1. One question A player's save file has a value out of range. Which way of dealing with it fits?

    Choose one answer.

    Show the answer: One question

    Answer: Refuse the file by returning an Error, and let the caller tell the player

    Check data where it enters the game, and hand the decision to the caller with an Error.

assert, push_error and push_warning, each run to its end Pro: opens with a Pro, Premium or Ultimate pass

The three reporting tools look alike in a log and behave very differently in a running game. This part runs each one, reproduces the headless run that never ends after a failed assert, and shows what changes in a release export.

In this part:

  • An assert stops its function, and an error report lets it go on
  • An assert before quit() leaves a headless run waiting for ever
  • In a release export, the code inside assert() never runs

3 runnable examples (GD, Python) · about 4 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).

A settings loader that checks every value before using it Pro: opens with a Pro, Premium or Ultimate pass

A settings file is written by the game and edited by players, so it is untrusted input every time it loads. This part builds a loader that refuses each kind of bad file with its own Error, and keeps the game’s settings untouched until the whole file has passed.

In this part:

  • Seven files, six of them wrong, and what the loader returns for each
  • Check everything first, then change anything

1 diagram · 3 runnable examples (GD, Python) · about 4 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).

Every error in a log of your own, and a loader to build Pro: opens with a Pro, Premium or Ultimate pass

Errors that players meet are lost unless the game keeps them. This part collects every error and warning with a custom Logger, makes a check fail with the right exit status, and ends with a lab whose tempting solution reports OK on a broken file.

In this part:

  • A custom Logger keeps each error with its script line
  • Lab: a difficulty loader that never reports OK by mistake

7 runnable examples (GD, GODOT, Python) · about 5 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).

Practise

These questions cover the first three sections, runtime errors, Error codes and the four tools; the weakest idea comes first and every answer explains itself:

Practice that adapts to you

Practise this lesson

9 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 9 What does GDScript do with a runtime error, such as calling a method on null?

    Choose one answer.

    Show the answer: Question 1 of 9

    Answer: It stops the current function, prints the error with a backtrace and lets the caller go on

    One function stops; everything else goes on.

  2. Question 2 of 9 gold_each(gold: int, players: int) -> int returns gold / players. What does gold_each(90, 0) return?

    Type a number.

    Show the answer: Question 2 of 9

    Answer: 0 gold

    Dividing by zero is a runtime error, so the function stops and returns the default of an int, 0, which the caller printed as gold each: 0.

  3. Question 3 of 9 A backtrace reads [0] damage_to (res://stops_one_function.gd:6), [1] _init (res://stops_one_function.gd:26). Where is the cause most likely to be fixed?

    Choose one answer.

    Show the answer: Question 3 of 9

    Answer: Where _init() at line 26 passes null, or in damage_to() with a check before line 6

    Read a backtrace from the top: the error, then each caller in turn, until you reach the code that made the bad value.

  4. Question 4 of 9 What does error_string(43) return?

    Choose one answer.

    Show the answer: Question 4 of 9

    Answer: "Parse error"

    error_string() turns any Error into its name in words, for messages and logs.

  5. Question 5 of 9 config.load("user://controls.cfg") returns 7 and the code goes on to call config.get_value("keys", "jump", "Space"). What does get_value() give?

    Choose one answer.

    Show the answer: Question 5 of 9

    Answer: "Space", the default, because nothing was loaded

    An ignored Error lets the game run on defaults without a word, far from the cause; check the code where it comes back.

  6. Question 6 of 9 json.parse(text) returned ERR_PARSE_ERROR for a player's settings file. What should the message for the player or the log contain?

    Choose one answer.

    Show the answer: Question 6 of 9

    Answer: error_string(err) and json.get_error_message(), here Parse error and Expected value, got 'EOF'

    Report what failed and why; keep the defaults; never stop the game for a settings file.

  7. Question 7 of 9 What is assert() for?

    Choose one answer.

    Show the answer: Question 7 of 9

    Answer: Checking an assumption of your own code, in debug runs, so that a mistake stops its function loudly

    An assert states something that must always be true, so it fails only when your own code is wrong.

  8. Question 8 of 9 A function calls push_error("too many bats") and then return 50. What happens?

    Choose one answer.

    Show the answer: Question 8 of 9

    Answer: The error is printed with a backtrace, and the function returns 50

    Use push_error() when the function can carry on with a safe value but someone must hear about the problem.

  9. Question 9 of 9 A check of the player's save is written as assert(save.version == 3, "old save"). What happens in a release export?

    Choose one answer.

    Show the answer: Question 9 of 9

    Answer: Nothing; the code inside assert() is not run, so an old save goes through unchecked

    Checks on data from players must be ordinary code that every build runs, with an Error returned.

Interview questions

Warm-up: what happens when a GDScript function hits a runtime error?

GDScript has no exceptions, so nothing can be caught. The function stops at the line with the error, Godot prints SCRIPT ERROR with a backtrace of the calls that led there, and the function returns the default value of its return type: 0 for an int, an empty String, null for an object. The caller carries on with that value as if it were real, and the run’s exit status does not change. In the editor the debugger also pauses the game at that line. So code that must not go on with a default value checks its inputs before the line that can fail.

  1. Fresher to mid

    A settings loader declared as load_from(path) -> Error returns OK, yet the game starts with the wrong enemy count, and the log shows a SCRIPT ERROR from inside the loader. How can both be true, and how would you restructure the loader?

    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

    In every playtest the crypt door opens with its key, but players of the released game say it never opens. The door calls assert(inventory.use_key("crypt")), and use_key() removes the key and marks the crypt as open. What is going on, and how do you fix it and find bugs like it?

    Model answer, Pro. First step and follow-ups

  3. Senior to staff

    Your released game misbehaves on some players' machines, and you hear about it only through vague reviews. Design how the game records its errors and gets them back to you, and say what you would not collect.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • A runtime error stops only the function it happens in; the function returns its type’s default, and the caller goes on with it.
  • Engine functions such as ConfigFile.load() return an Error code; test it against OK and report it with error_string().
  • assert() checks your own assumptions and stops its function in debug runs only, push_error() and push_warning() report and go on, and a returned Error lets the caller decide.
  • Check data where it enters the game, files, saves, network messages and inspector values, and trust your typed code inside it.

Terms in this lesson

error code
A value of Godot's Error enum that many engine functions return instead of stopping: OK is 0, and every failure is a positive number, such as 7 for ERR_FILE_NOT_FOUND. error_string() turns a code into words.
runtime error
A mistake found while a line runs, such as calling a method on null or reading past the end of an array. GDScript stops the function it happens in, prints SCRIPT ERROR with a backtrace and lets the caller go on.

References

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.