Error Handling

Available by default.

There is no support for exceptions of any kind in BL, however, there are a few features in the language helping with error handling such as defer and multiple return values. In general, every possibly failing function should return an error state in case of failure containing an error code and some message describing the problem. This style is widely adopted by the standard library and may be used in the user code as well.

The error state is represented by the following data structure:

_Error :: struct {
    msg: string_view;
    code: s32;
}

Error :: *_Error;

Where Error is just pointer to thread-local allocated storage (shared for all error states) used as temporary storage for the actual _Error data.

The null pointer to Error means no error, we can use OK constant instead typing null every time.

Catch

You can grab the error value as any other returned value on a function call-side and check if error is present by a simple if statement.

err :: open_file(filepath);
if err then print_err(err);

// Or invoke panic?
if err then panic(err);

Other option to handle error on call-side is using the catch statement, which implicitly does error presence check for you. The error value is captured in $ builtin variable. The catch statement checks the last member in returned value; meaning, when function returns multiple values, the error state is supposed to be the last.

Example

#import "std/print"
#import "std/fs"

// This function will load provided file and tries to print its content. Error is returned
// in case of any failure.
print_file_content :: fn (filepath: string_view) Error {
	content :: read_entire_file(filepath) catch return $;
	defer free_slice(&content);
	print(content);

	return OK;
}

div :: fn (a: s32, b: s32) (s32, Error) {
	if b == 0 {
		// Return error with message.
		return 0, error("Divide by zero '%/%'!", a, b);
	}
	// Return result and OK state.
	return a / b, OK;
}


main :: fn () s32 {

	// Call print_file_content, the catch branch is executed in case there is an
	// error returned by the function.
	print_file_content("some_invalid_file_path") catch {
		// Use $ builtin symbol to access the captured error.
		print_err($);
	};

	print_file_content(#file) catch {
		print_err($);
	};

	// In case of multi-return value, error is expected to be last in the return
	// list.
	result :: div(10, 0) catch {
		print_err($);
	};
	print("result = %\n", result);

	return 0;
}

As already mentioned, the defer statement may be helpful in situations where we need to free some memory or do any kind of resource cleanup before the failing function returns the error state.

Error

Error :: *_Error

File: error.bl

OK

OK : Error = null

No error.

File: error.bl

is_error

is_error :: fn (err: Error, code: s32) bool #inline

Check whether err is representing error code. Returns false when err is null.

File: error.bl

error

error :: fn { 
    impl_error1; 
    impl_error2; 
    impl_error3; 
}

Overloaded function setting up error state. Error state is global variable holding Error instance, error function sets desired values and return pointer to this global. That means the Error must be handled immediately after it's returned from failing function since every Error state points to the same memory.

Error creating does not require any HEAP memory alocations.

Overloads:

fn (code: s32) Error #inline
fn (format: string, args: ...) Error #inline
fn (code: s32, format: string, args: ...) Error #inline

Sets error state with code and formatted message.

File: error.bl