Capy-Bearer Documentation

13. Errors and testing

Capy reports type and syntax errors before a unit runs. Runtime failures include the original Capy source location.

Handle expected missing data

Use a fallback when absence is valid:

function display_name(profile : any) string
{
	-> string(profile.name, fallback: "guest")
}

function RENDER()
{
	print(display_name({}), "\n")
}

Output

guest

The fallback handles both none and failed string conversion. It does not hide a compiler error or an unrelated runtime trap.

Handle operations that can fail

Some operations can fail when the program is correct: a file is missing, a server does not answer, or outside data is not valid JSON. These functions return a result and do not stop the request:

Files       file_get_contents
            file_put_contents  file_append  file_copy  file_rename
            file_truncate  file_chmod  file_symlink  file_unlink
Decoding    json_decode  yaml_decode  xml_decode  brb_decode
            base64_decode  php_decode  php_encode
Archives    gz_uncompress  zip_list  zip_read  zip_create  zip_extract
Databases   mysql_connect  sqlite_connect
Network     http_request  fastcgi_request
Processes   shell_exec with options  task_execute
Crypto      crypto_operation

A result is an any map. On success, ok is true and value holds what the function produced. On failure, ok is false and error tells what failed. Some failures also have other fields, such as error_code. A function that only does an action, such as file_put_contents() or zip_create(), returns ok and error and no value.

Use try to handle both cases. The guide on expressions and control flow explains the form of try:

function age(text : string) string
{
	-> try data := json_decode(text)
	{
		-> string(data.age, fallback: "unknown")
	}
	else
	{
		-> "invalid: " + string(data.error)
	}
}

function RENDER()
{
	print(age("{\"age\": 36}"), "\n")
	print(age("{age"), "\n")
}

Output

36
invalid: json_decode: invalid JSON at byte 1

When a default value is enough, read value with ??. On failure, value is none, so file_get_contents(path).value ?? "" gives the file text or an empty string.

Give a failure to the caller

A function that cannot handle a failure can return the result. The caller then handles all failures in one place:

function read_settings(path : string) any
{
	-> try text := file_get_contents(path)
	{
		-> json_decode(string(text))
	}
	else
	{
		-> text
	}
}

function RENDER()
{
	file_put_contents("/tmp/doc-settings.json", "{\"title\": \"Docs\"}")
	try settings := read_settings("/tmp/doc-settings.json")
	{
		print(settings.title, "\n")
	}
	try missing := read_settings("/tmp/doc-settings-missing.json")
	{
		print(missing.title, "\n")
	}
	else
	{
		print(missing.error, "\n")
	}
}

Output

Docs
file_get_contents: cannot read /tmp/doc-settings-missing.json: missing or outside the readable roots

In the else block of read_settings, text is the failed result of file_get_contents(), so the caller gets that error. On success, the function returns the result of json_decode(), which can also be a failure.

Where a function documents fixed codes, such as the error_code of fastcgi_request() or the error codes of task_execute(), compare those codes. Do not compare the text of an error message.

Compiler errors

The compiler rejects invalid syntax, unknown names, and incompatible types. For example, this assignment is invalid:

function RENDER()
{
	var count := 1
	count = "one"
}

Output

/work/site/tests/doc-context.81191375c7ca475c86c6593020140c76/example.capy:4:8: expected s64, found string

count has type s64. Assignment cannot change its type to string.

Read the first diagnostic and its source location. Later messages can be consequences of the first error.

See compiler errors in the browser

The setting defaults to 0. Set it to 1 in /etc/bearer/settings.cfg to show diagnostics on the runtime error page:

SHOW_DYNAMIC_COMPILE_ERRORS=1

A compile failure stops the request and returns HTTP 500. This applies to entry units, components, unit_call(), imported aliases, and units loaded with unit_load().

The unit configured by page_runtime_error receives the error in request.call.error. With diagnostics enabled, a compile error has these fields:

  • type is compile_error.

  • title and details contain the error summary.

  • unit is the resolved unit path.

  • compiler_messages contains the full compiler output.

  • location.file, location.line, and location.column identify the first diagnostic.

With the default setting, the error contains only type and title. The plain response contains only the runtime error label and HTTP status. The server log still receives the full diagnostic.

Nginx can replace Bearer's error response when error interception is active. Keep interception off in the FastCGI location:

fastcgi_intercept_errors off;

Runtime traps

Use strict access when missing data violates the program contract:

function CLI()
{
	var profile := {}
	if !has(profile, "name")
	{
		trap()
	}
	var name := get(profile, "name")
	print(name)
}

Output

HTTP 500
BEARER CLI wasm error: wasm trap: wasm `unreachable` instruction executed
backtrace (5 frames):
  #0 0x410 example.capy.wasm!<wasm function 16>
  #1 0x51a example.capy.wasm!<wasm function 17>
  #2 0xabfe8 core.wasm!wasm_invoke_handler(void (*)(void*, void*), DValue*, DValue*)
  #3 0xb2f7d core.wasm!wasm_invoke_loaded(int, int, DValue*)
  #4 0xb2749 core.wasm!bearer_wasm_invoke_loaded_entry
source locations:
  #0 wasm function at tests/doc-context.6d41464f30564a7285724897e04a743d/example.capy:3:17
  #1 wasm function at tests/doc-context.6d41464f30564a7285724897e04a743d/example.capy:1:1

This traps if name is missing or the access path is unusable. Other traps include division by zero, invalid typed-array indexes, invalid arguments to a standard function, and an explicit trap().

Do not use a fallback for data that the program requires. A clear trap at the boundary is safer than incorrect work with a fabricated value.

Test through Bearer

Run one page through the local CLI socket:

scripts/bearer-cli /path/to/page.capy

The CLI helper exposes the response body and runtime errors. Use an HTTP client or a repository HTTP test helper when a test must verify status and headers. Include failure cases for missing input, invalid conversions, and boundary errors.

The repository test suites exercise the compiler, Wasm runtime, source maps, components, units, tasks, and documentation examples:

scripts/test_capy_native.sh
scripts/test_capy_phase1.sh
scripts/test_capy_reference.sh

Run the focused test while editing. Run the broader gate before deployment.

Keep code direct

Prefer a small typed helper over repeated dynamic conversion. Keep request parsing near the request boundary. Convert an any to a static type once the required shape is known.

Use one name for one thing. Remove wrappers that only rename another function. Keep comments for behavior that the code cannot make clear.

Find API details

The language guide explains how Capy code fits together. The API reference gives exact standard-library signatures and failure behavior. Use the generated Capy signature on each API page.

Read failures as contracts

A compile error usually means the source does not match the type contract. Fix that error before you inspect later messages.

A result with ok: false reports a failure outside the program, such as a missing file or invalid input data. Handle it where the program can do something useful: show a message, use a default, or return the result to the caller.

A runtime trap usually means the program broke its own contract. Test required data with has() before you call get(). Use constructors with fallbacks when absence is valid.

Write tests for both paths. A success test proves the expected shape works. A failure test proves bad input cannot silently continue.