到目前為止,我們把重心放在一些酷酷的地方(寫了很多功能與演算法)但錯誤可觀測性也重要。WASM 這一端如果只是 return Err(JsValue::from_str("something bad"))
,前端拿到的就會是一條字串,既不利於分流處置,也不便於觀測串接。今天把錯誤合成一個漂亮的介面:Rust 端回傳 Result<T, JsValue>
,至少帶兩個欄位:{ code, message }
。code
是穩定機器碼,message
是給人看的說明,方便在 UI、Log、監控上做對應處理。
我在 Rust 內部定義一個 ErrorCode
列舉與輕量結構 JsError { code, message }
;所有導出的 #[wasm_bindgen]
函式都只回傳 Result<..., JsValue>
,失敗時透過 serde_wasm_bindgen::to_value(&JsError{...})
序列化成真正的 JS 物件。這樣前端 catch
到的就是 { code, message }
,可以像一般 API 失敗一樣判斷、顯示、上報。
把 Err 都變成 {code, message}
物件
use wasm_bindgen::prelude::*;
use serde::{Deserialize, Serialize};
use serde_wasm_bindgen as swb;
// 1) 定義錯誤碼
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
enum ErrorCode {
InputLengthMismatch,
BadOps,
Unsupported,
Internal,
}
// 2) 傳給 JS 的錯誤物件
#[derive(Clone, Debug, Serialize, Deserialize)]
struct JsError<'a> {
code: ErrorCode,
message: &'a str,
}
// 3) 把 (code, message) 轉成 JsValue
fn js_err(code: ErrorCode, message: &str) -> JsValue {
swb::to_value(&JsError { code, message }).unwrap_or_else(|_| {
JsValue::from_str(message)
})
}
接著,把公開 API 的 Err
都換成這個格式。
#[wasm_bindgen]
pub fn apply_pipeline(input: &[u8], w: u32, h: u32, ops: &JsValue) -> Result<Vec<u8>, JsValue> {
let expected = (w as usize) * (h as usize) * 4;
if input.len() != expected {
return Err(js_err(ErrorCode::InputLengthMismatch, "input length mismatch"));
}
let ops: Vec<Op> = swb::from_value(ops.clone())
.map_err(|e| js_err(ErrorCode::BadOps, &format!("bad ops: {e}")))?;
// ...照舊處理,遇到不支援的分支可以回報:
// return Err(js_err(ErrorCode::Unsupported, "unsupported op: ..."));
let mut buf = input.to_vec();
for op in ops {
buf = match op {
Op::Grayscale => grayscale(&buf, w, h),
Op::BrightnessContrast { b, c } => brightness_contrast(&buf, w, h, b, c),
Op::Blur { r } => box_blur_rgba(&buf, w, h, r),
Op::Conv3x3 { k } => convolve3x3(&buf, w, h, &k),
Op::Unsharp { r, amount, threshold, limit } =>
unsharp(&buf, w, h, r, amount, threshold, limit),
Op::EdgeSobel => edge_sobel(&buf, w, h),
};
}
Ok(buf)
}
#[wasm_bindgen]
pub fn apply_pipeline_fast(input: &[u8], w: u32, h: u32, ops: &JsValue) -> Result<Vec<u8>, JsValue> {
let expected = (w as usize) * (h as usize) * 4;
if input.len() != expected {
return Err(js_err(ErrorCode::InputLengthMismatch, "input length mismatch"));
}
let ops: Vec<Op> = swb::from_value(ops.clone())
.map_err(|e| js_err(ErrorCode::BadOps, &format!("bad ops: {e}")))?;
let mut a = input.to_vec();
let mut b = vec![0u8; expected];
let mut toggle = false;
for op in ops {
match op {
Op::Grayscale => { if !toggle { grayscale_into(&a, &mut b); } else { grayscale_into(&b, &mut a); } }
Op::BrightnessContrast { b: br, c } => {
if !toggle { brightness_contrast_into(&a, &mut b, br, c); }
else { brightness_contrast_into(&b, &mut a, br, c); }
}
Op::Blur { r } => {
if !toggle { box_blur_rgba_into(&a, &mut b, w, h, r); }
else { box_blur_rgba_into(&b, &mut a, w, h, r); }
}
Op::Conv3x3 { k } => {
if !toggle { convolve3x3_into(&a, &mut b, w, h, &k); }
else { convolve3x3_into(&b, &mut a, w, h, &k); }
}
Op::Unsharp { r, amount, threshold, limit } => {
if !toggle { unsharp_into(&a, &mut b, w, h, r, amount, threshold, limit); }
else { unsharp_into(&b, &mut a, w, h, r, amount, threshold, limit); }
}
Op::EdgeSobel => {
if !toggle { edge_sobel_into(&a, &mut b, w, h); }
else { edge_sobel_into(&b, &mut a, w, h); }
}
}
toggle = !toggle;
}
Ok(if toggle { b } else { a })
}
#[wasm_bindgen]
pub fn apply_pipeline_into(
input: &[u8],
w: u32,
h: u32,
ops: &JsValue,
out: &mut [u8],
) -> Result<(), JsValue> {
let expected = (w as usize) * (h as usize) * 4;
if input.len() != expected || out.len() != expected {
return Err(js_err(ErrorCode::InputLengthMismatch, "input or output length mismatch"));
}
let ops: Vec<Op> = swb::from_value(ops.clone())
.map_err(|e| js_err(ErrorCode::BadOps, &format!("bad ops: {e}")))?;
// 這裡示範最簡單的「A=輸入, B=out」乒乓
let mut a = input.to_vec();
let b_ptr = out as *mut [u8] as *mut u8; // 只是為了清楚指出 out 是目的端
let mut toggle = false;
for op in ops {
match op {
Op::Grayscale => { if !toggle { grayscale_into(&a, out); } else { grayscale_into(out, &mut a); } }
Op::BrightnessContrast { b: br, c } => {
if !toggle { brightness_contrast_into(&a, out, br, c); }
else { brightness_contrast_into(out, &mut a, br, c); }
}
Op::Blur { r } => {
if !toggle { box_blur_rgba_into(&a, out, w, h, r); }
else { box_blur_rgba_into(out, &mut a, w, h, r); }
}
Op::Conv3x3 { k } => {
if !toggle { convolve3x3_into(&a, out, w, h, &k); }
else { convolve3x3_into(out, &mut a, w, h, &k); }
}
Op::Unsharp { r, amount, threshold, limit } => {
if !toggle { unsharp_into(&a, out, w, h, r, amount, threshold, limit); }
else { unsharp_into(out, &mut a, w, h, r, amount, threshold, limit); }
}
Op::EdgeSobel => {
if !toggle { edge_sobel_into(&a, out, w, h); }
else { edge_sobel_into(out, &mut a, w, h); }
}
}
toggle = !toggle;
}
// 如果最後結果在 a,就複回 out(確保呼叫者拿到的是最新)
if !toggle {
out.copy_from_slice(&a);
}
Ok(())
}
你可以看到,除了把 Err(...)
的生成改成 js_err(...)
,其餘的完全沒變。
前端這邊,我建議做一個簡單的 type guard,統一處理三種情形:
{ code, message }
;type WasmError = { code: string; message: string }
function isWasmError(e: unknown): e is WasmError {
return !!e && typeof e === 'object'
&& 'code' in (e as any) && 'message' in (e as any)
&& typeof (e as any).code === 'string'
&& typeof (e as any).message === 'string'
}
function showError(e: unknown) {
if (isWasmError(e)) {
// 機器碼可用於分流/上報;訊息給人看
console.error(`[WASM] ${e.code}: ${e.message}`)
alert(`${e.code}\n${e.message}`)
} else if (typeof e === 'string') {
console.error('[WASM]', e)
alert(e)
} else {
console.error('[WASM] Unknown error', e)
alert('Unknown error')
}
}
把 runPipeline
、runPipelineInto
、或任何呼叫包成 try/catch,錯誤直接丟給 showError
:
const runPipeline = (ops: unknown[], useFast = true) => {
if (!w || !h) return
const imgData = ctx.getImageData(0, 0, w, h)
const input = new Uint8Array(imgData.data.buffer)
try {
const pipe = useFast ? fastPipe : slowPipe
const out = pipe(input, w, h, ops) as Uint8Array
imgData.data.set(out)
ctx.putImageData(imgData, 0, 0)
} catch (e) {
showError(e)
}
}
這樣 UI 能根據 code
做分支:例如 InputLengthMismatch
直接提示「圖片尺寸或像素長度不符」;BadOps
顯示「參數格式錯誤」;Unsupported
可以偵錯 OPS 內容;Internal
則走一般的錯誤回報流程。