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
guestThe 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_operationA 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 1When 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 rootsIn 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 stringcount 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=1A 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:
typeiscompile_error.titleanddetailscontain the error summary.unitis the resolved unit path.compiler_messagescontains the full compiler output.location.file,location.line, andlocation.columnidentify 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:1This 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.capyThe 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.shRun 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.