본문 바로가기
C.W.K.
Stream
Lesson 06 of 07 · published

경계를 건너는 에러

~14 min · tauri, errors, result, rust

Level 0웹 관광객
0 XP0/56 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete
"Ok은 resolve로, Err은 reject로 도착해. 실패를 Result로 모델링해두면 프론트엔드는 깔끔하게 잡히는 에러를 공짜로 받아."

Result가 메커니즘 전부야

실패할 수 있는 command는 Result<T, E>를 돌려줘. Ok(value)를 돌려주면 프론트엔드 Promise가 value로 resolve되고, Err(e)를 돌려주면 Promise가 reject돼서 JS 쪽 try/catch.catch()가 받아. 따로 배울 에러 채널 같은 건 없어 — 이미 아는 그 타입이 에러 프로토콜이야.

에러 타입도 직렬화돼야 해

에러도 다리를 건너니까 E는 직렬화가 돼야 해. 빠른 길은 Result<T, String>이야 — .map_err(|e| e.to_string())로 어떤 실패든 메시지로 바꿔버리면 돼. 튼튼한 길은 Serialize를 derive한 커스텀 에러 enum이고. 그러면 프론트엔드가 실패의 종류(못 찾음이냐, 권한 없음이냐, 네트워크냐)를 구분해서 다르게 반응할 수 있어. thiserror 같은 크레이트를 쓰면 그런 에러 타입을 몇 줄로 정의해.

? 연산자가 깔끔하게 유지해줘

Result를 돌려주는 command 안에서 ? 연산자는 Ok면 알맹이를 꺼내고 Err면 즉시 반환해 — From 구현이 있으면 가는 길에 에러 타입까지 바꿔주고. 겹겹이 쌓인 match 문이 위에서 아래로 읽히는 코드가 되는 거야. 커스텀 에러 enum이랑 thiserror#[from]을 같이 쓰면, 실패할 수 있는 연산들이 각자 뒤에 ? 하나씩만 달고 한 줄기로 이어져.

Code

타입이 붙은, 직렬화되는 에러·rust
use serde::Serialize;
use thiserror::Error;

// 프론트엔드가 분기할 수 있는 커스텀 에러. derive(Serialize)로 건너게 해.
#[derive(Debug, Error, Serialize)]
#[serde(tag = "kind", content = "message")]
enum NoteError {
    #[error("note {0} not found")]
    NotFound(u32),
    #[error("disk error: {0}")]
    Io(String),
}

#[tauri::command]
fn get_note(id: u32) -> Result<String, NoteError> {
    let raw = std::fs::read_to_string(format!("notes/{id}.txt"))
        .map_err(|e| NoteError::Io(e.to_string()))?; // Err이면 ?가 조기 반환
    if raw.is_empty() {
        return Err(NoteError::NotFound(id));
    }
    Ok(raw)
}
웹 쪽에서 받아내기·tsx
// 프론트엔드에선 Err이 reject된 Promise로 도착.
try {
  const note = await invoke<string>("get_note", { id: 99 });
} catch (err) {
  // err은 직렬화된 NoteError: { kind: "NotFound", message: 99 }
  console.error("could not load note:", err);
}

External links

Exercise

Result<String, String>을 돌려주면서 어떤 입력엔 실패하는(이를테면 빈 인자면 Err) command를 짜봐. 프론트엔드에서 try/catch 안에 넣어 부르고 에러 메시지를 UI에 그려. 보너스: 에러를 derive(Serialize) 붙인 작은 enum으로 올리고 에러 종류에 따라 UI를 갈라봐.
Hint
제일 단순한 실패는 이거야: if name.trim().is_empty() { return Err("name required".into()); }. 프론트엔드 catch 블록의 err이 네가 Err에 넣은 그 값이야. 보너스는 #[serde(tag="kind")]가 TS에서 갈라 쓰기 좋은 모양을 만들어줘.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.