에러 언랩 시도

에러 언랩 시도

에러를 돌려줄 수 있는 함수에서 catch unreachable로 값을 바로 풀었는데, 그 함수가 실제로 에러를 뱉어버리면 어떻게 될까요? Zig는 조용히 넘어가지 않고 컴파일 타임이든 런타임이든 분명하게 실패시켜요. 이번 섹션에서는 그 예외 상황을 하나씩 살펴볼게요.

출처: Zig Documentation

본문

getNumberOrFail()!i32 타입, 그러니까 i32를 돌려주거나 에러를 돌려줄 수 있는 함수예요. 그런데 이 함수는 항상 error.UnableToReturnNumber를 돌려줍니다. 그런 함수의 결과를 catch unreachable로 풀어버리면, 에러가 실제로 발생하는 순간 문제가 생겨요. 언제냐에 따라 실패 방식이 달라져요.

컴파일 타임에는

comptime {
    const number = getNumberOrFail() catch unreachable;
    _ = number;
}

fn getNumberOrFail() !i32 {
    return error.UnableToReturnNumber;
}

Shell:

$ zig test test_comptime_unwrap_error.zig
/home/ci/work/zig-bootstrap/zig/doc/langref/test_comptime_unwrap_error.zig:2:44: error: caught unexpected error 'UnableToReturnNumber'
    const number = getNumberOrFail() catch unreachable;
                                           ^~~~~~~~~~~
/home/ci/work/zig-bootstrap/zig/doc/langref/test_comptime_unwrap_error.zig:7:18: note: error returned here
    return error.UnableToReturnNumber;
                 ^~~~~~~~~~~~~~~~~~~

컴파일 타임 연산은 값이 다 정해져 있으니, 컴파일러가 **"caught unexpected error 'UnableToReturnNumber'"**라는 오류를 내면서 빌드를 그 자리에서 막아요. catch unreachable은 '성공이라고 보고, 에러는 절대 오지 않는다'고 약속하는 거라서, 그 약속이 깨지는 순간 컴파일 오류가 되는 거예요.

런타임에는

const std = @import("std");

pub fn main() void {
    const number = getNumberOrFail() catch unreachable;
    std.debug.print("value: {}\n", .{number});
}

fn getNumberOrFail() !i32 {
    return error.UnableToReturnNumber;
}

Shell:

$ zig build-exe runtime_unwrap_error.zig
$ ./runtime_unwrap_error
thread 974265 panic: attempt to unwrap error: UnableToReturnNumber
error return context:
/home/ci/work/zig-bootstrap/zig/doc/langref/runtime_unwrap_error.zig:9:5: 0x11e828c in getNumberOrFail (runtime_unwrap_error.zig)
    return error.UnableToReturnNumber;
    ^

stack trace:
/home/ci/work/zig-bootstrap/zig/doc/langref/runtime_unwrap_error.zig:4:44: 0x11e82f3 in main (runtime_unwrap_error.zig)
    const number = getNumberOrFail() catch unreachable;
                                           ^
/home/ci/work/zig-bootstrap/out/host/lib/zig/std/start.zig:788:64: 0x11e7bbb in callMain (std.zig)
    if (fn_info.param_types.len == 0) return wrapMain(root.main());
                                                               ^
/home/ci/work/zig-bootstrap/out/host/lib/zig/std/start.zig:248:5: 0x11e75e1 in _start (std.zig)
    asm volatile (switch (native_arch) {
    ^
(process terminated by signal)

런타임에는 컴파일러가 미리 알 수 없으니, 에러가 실제로 도착하는 순간 **"attempt to unwrap error: UnableToReturnNumber"**라는 메시지와 함께 패닉이 나고 프로세스가 종료돼요. 잘못된 값을 조용히 넘겨서 프로그램을 계속 돌리는 대신, 에러가 난 위치를 스택 트레이스로 남겨서 문제를 바로 찾게 해 주는 거죠.

if로 에러를 미리 확인하기

이런 충돌을 피하는 한 가지 방법은 성공을 전제하는 대신 if 표현식으로 에러인지부터 확인하는 거예요.

const print = @import("std").debug.print;

pub fn main() void {
    const result = getNumberOrFail();

    if (result) |number| {
        print("got number: {}\n", .{number});
    } else |err| {
        print("got error: {s}\n", .{@errorName(err)});
    }
}

fn getNumberOrFail() !i32 {
    return error.UnableToReturnNumber;
}

Shell:

$ zig build-exe testing_error_with_if.zig
$ ./testing_error_with_if
got error: UnableToReturnNumber

if (result) |number|로 성공하면 값을, else |err|로 에러면 에러 객체를 받아서 각각 다르게 처리해요. 이번에는 에러가 와서 got error: UnableToReturnNumber가 출력됐지만, 어떤 경우에도 프로그램이 터지지 않고 두 경로를 안전하게 나눠 처리할 수 있어요.

더 알아보기