วิธีรับมือ Dev มือใหม่ เวลาใช้ Axios

*หมายเหตุ ตอนนี้พยายามเขียนบทความให้มากขึ้น เลยพยายามเขียนให้เร็ว อาจจะมีการตกล่นอะไร โปรดแจ้งได้เลยนะครับผมจะแก้ไขให้เร็วที่สุด

ปัญหาจากมือใหม่เวลาใช้ Axios

ผมคิดว่าสำหรับผู้พัฒนาเว็บไซต์ คงไม่มีใครไม่รู้จัก Axios [axios-http.com] วิธีใช้ก็แสนง่ายตามตัวอย่างใน Document

สรุปสั้นๆ Axios จะ ส่งค่าทุกอย่างในรูปแบบ Promise ถ้า Status 200 ก็ลง Then นอกเหนือจากนั้น ก็ลง Catch

ซึ่งก็ดูปกติดี… แต่เดี่ยวก่อน…

ปัญหาแรก

คือ Axios เอานอกเหนือจาก Status 200 ลง Catch หมดนี่ล่ะครับ

ซึ่ง Backend ที่ดีจะไม่ได้ใช้แค่ Status 200 ในการส่งค่ากลับมายัง Frontend

และใน Error ก็มี ข้อมูล Response ส่งกลับมาเตือนผู้ใช้ด้วยด้วย อาทิเช่น บอกว่า คุณใส่ password ผิด , email นี้มีคนใช้แล้ว , ไม่มีข้อมูลที่คุณค้นหา

เราเรียกว่า Error Response มันคือค่าที่ส่งกลับมาปกติ แต่มันอยู่ใน error.response แทน

ขอยืมรูปตัวอย่างจากบทความคุณ Brian Mulloy จะเห็นได้ว่าต่อให้ 200 , 400 ก็มี Response ที่ Backend พยายามส่งมาอยู่

https://cloud.google.com/blog/products/api-management/restful-api-design-what-about-errors

แล้วผู้พัฒนามือใหม่ชอบลืมจัดการตรงนี้ โดยไม่ได้สนใจว่า Error นั้นมี Response หรือไม่ จากมี Response กลายเป็น Error แบบไม่มี Response ทั้งหมด ย้ำรูปนี้อีกที Error ทั้งหมดก็คือ Error

ย้ำรูปนี้อีกที Error ทั้งหมดก็คือ Error

ปัญหาที่ 2

ที่จะชอบเกิดกับมือใหม่ คือ Try-Catch Hell แปลตามตัว ขุมนรก Try-Catch โดยการเอา Logic ยาวๆ ไปใส่ไว้ใน Try (ก็ตัวอย่างทางการแนะนำมา) มีปัญหาอะไรก็ Try Catch วนไป ดังตัวอย่าง

try {
const response = await Axios.get(url);
// ... โค๊ด Logic ซัก 100 บรรทัด try { // มีปัญหาซักอย่าง เลยต้อง Try-Catch
const data = helperFunction(response);
try { // มีปัญหาซักอย่างก็ Try-Catch Hell วนไป
showData(data);
} catch (error: any) {
//...
}
} catch (error: any) {
// ...
}
} catch (error: any) {
console.log(error);
}

พอเกิด Catch จาก Axios ฝั่งผู้พัฒนามือใหม่ก็ชอบ Try Catch แบบเหมาคลุม Logic แบบยาวๆมากๆ แล้วก็ชอบทำ Try-Catch ซ้อนไปเรื่อยๆด้วย

ซึ่ง Try Catch เวลามันพัง มันจะย้อนโค๊ดกลับไปทีละบรรทัด เหมือนย้อนเวลา แล้วยิ่งบรรทัดเยอะเท่าไหร่ก็จะยิ่งหน่วงมากๆครับ แบบ Response เร็ว แต่ Error ดันรอเป็น 10 วินาทีก็มีกันเลยทีเดียว

วิธีแก้ปัญหา

ผมเลยต้องเขียน Function Helper ไว้ให้ใช้ ซึ่งหน้าตาจะประมาณนี้ครับ โดยดึงแนวความคิดของ Golang มา ที่จะส่งค่า Response ไปพร้อมกับ Error กลับไป

// โยน Axios Promise เข้ามาได้เลย
export const helperAxiosAPI = async (promiseAPI: any) => {
return promiseAPI.then(
(response: any) => {
// ถ้าผ่าน ก็ส่ง response กลับไป ไม่ส่ง response.data เพื่อให้ dev เช็ค status กับ header ได้
return { res: response, err: null };
}
).catch((error: any) => {
// ถ้าไม่ผ่านแต่มี response.data ก็นับว่าเป็น response อยู่แล้วค่อยเอา response ไปเช็คข้างในอีกที
if (error.response && typeof error.response.data == 'object') {
// ไม่ส่ง response.data เพื่อให้ dev เช็ค status กับ header ได้
return { res: error.response, err: error };
}

// อันนี้ Error เพียวๆเลย Res ก็ไม่มี
return { res: null, err: error };
});
};

// พอดีลองเขียนสดน่าจะประมาณนี้ พอดีตัดส่วนที่ช่วยเหลือผู้พัฒนา จากตัวจริงไปพอสมควร เหลือแต่ส่วนหลักๆ ยังไม่ลอง Compile ถ้าผิดยังไงแจ้งได้ขออภัยครับ

โดยวิธีใช้

// โยน Axios ลงไปได้เลย ไม่ต้อง await 2 รอบ
const { res, err } = await helperAxiosAPI(Axios.get('url'));
// ถ้า Error แล้ว ไม่มี Response ก็ให้แสดง Error
if (err && !res) {
console.log('err', err);
return null
}
// ถ้าไม่มีข้อมูลกลับมาเลย แสดงผลอะไรซักอย่าง (มีหรือไม่มีก็ได้แล้วแต่ Logic)
if(!res){
console.log("Not Have Data");
return null
}
// ข้อมูลที่ต้องการ
console.log("Success:",res);

หรือคนที่ชอบ throw คือโยน Error ให้ Middleware จัดการ ก็

const { res, err } = await helperAxiosAPI(Axios.get('url'));// ถ้า Error แล้ว ไม่มี Response ก็ให้แสดง Error
if (err) {
throw err
}
// หรือจะ​ throw เฉพาะ Error จริงๆ
if (err && !res) {
throw err
}
// ที่เหลือ เหมือนเดิม
if(!res){
console.log("Not Have Data");
return null
}
// ข้อมูลที่ต้องการ
console.log("Success:",res);

เท่านี้ Axios ก็จะอ่านง่ายและดูดีขึ้นทันตาเห็นเลย ไม่มี Try-Catch Hell , Promise Hell , If-Else Hell ตรงไปตรงมาแบบ Golang

เพิ่มเติม อีกวิธีที่ใครคิดว่า Helper เกะกะลูกตา

มีคนแนะนำการใช้ interceptors เข้าช่วยใน axios เองจะได้ไม่มี function ครอบ

axios.interceptors.response.use( (res) =>{
return {res: res , err: null};
}, (error)=> {
if(error.response && typeof error.response.data == 'object'){
// ไม่ส่ง response.data เพื่อให้ dev เช็ค status กับ header ได้
return { res: error.response, err: error };
}
return {res: null , err: error}
});
// เวลาใช้ก็เรียกได้เลย
const { res, err } = await Axios.get('url');

สรุป ทำไปทำไม ทำแล้วได้อะไร

เนื่องจากมือใหม่ หรือ Junior มักจะทำงานแบบ Best Case Only ทำก่อนแก้ทีหลัง พอทำๆไป โค๊ดก็สุม คนที่มาทำต่อก็เข้าใจยาก โดยทำเพื่อ

  1. เบาแรง Dev ลดโค๊ดที่ใช้ซ้ำซาก จะได้น้อยลง จะได้เอาเวลาไปสนใจเฉพาะ Business Logic ต่างๆ แค่ส่วนนี้ก็เยอะแล้ว และถ้าไม่ใช้ก็ดีกว่าไม่มีเดฟไม่ต้องเอา res ที่ทำมาใช้ต่อก็ได้
  2. ลดการทำ Hell รูปแบบต่างๆ พอปล่อยให้ Dev มือใหม่จัดการเอง คือเละครับ เขียนแบบไม่ติด Hell ไม่เป็น กว่าจะเป็นต้องเจ็บตัวมาเยอะ ผมเลยดักไว้ก่อนไปเลย
  3. จากข้างต้นทำให้อ่านง่ายขึ้น เผื่อคนที่มาทำต่อจะได้ไม่ต้องมาปวดหัว
  4. ทำ Function เพื่ออำนวยความสะดวกเพิ่มทีหลังได้ ที่จริงๆ ตรง helperAxiosAPI ไม่มั่นใจ ไม่ได้ compile ลอง เพราะตัดทอนส่วนที่ช่วยต่างๆ เช่นเช็ค Validate Request , Pagination , Noti , Log ต่างๆ ที่ทำไว้เหลือเฉพาะส่วนสำคัญครับ

ถ้าชอบยังไงฝากแชร์ฝากแสดงความคิดเห็นหน่อยนะครับ
หรือฝากถูกใจเพจเฟสบุ๊คบริษัทต่างโลก อินเตอร์แอคทีฟ จำกัด ได้นะครับ
ขอบคุณครับ

เพิ่มเติม สำหรับคนสงสัยเรื่อง Try-Catch

เมื่อ Try-Catch ซ้อนกันหลายๆชั้น ถ้าชั้นที่ลึกสุด ดักได้แล้ว ชั้นบนๆ ก็ควรจะไม่ Catch ใช่หรือเปล่า จะช้าได้ไง

ใช่ครับ แต่ทั้งนี้ บางคน Catch แล้ว Throw Error ใหม่ขึ้นไปข้างบนเรื่อยๆ พออยู่บนสุด จะเข้า Middleware ที่จัดการ Error ให้สวยๆ ตัวอย่าง

catch(error: any){ throw new Error(“Not Have Data”);}

อัจฉริยะสุดๆ ทีนี้ย้อนกันมันเลยครับ ต้องมาดักอีกว่า Error จริง หรือ Error ที่ออกแบบเอง แล้วเราก็ไม่แน่ใจว่า Dev จะครอบ Try Catch สั้นสุดไหม ครอบยาวๆ ก็หน่วงยาวๆครับ

เพิ่มเติม 2

มีท่านนึงถามในกระทู้ มีโจทย์ ถ้าต้องทำ act1–3 จะสำเร็จหรือไม่ค่อยทำ act4 ตาม แบบข้างล่าง

try {
await act1()
await act2()
await act3()
} catch (e) {}
await act4()

แล้วทำเป็น เช็ค error โดยใช้ If แบบนี้มันไม่เป็น If-Else Hell หรือครับ

let err1 = await act1()
if (!err1) {
let err2 = await act2()
if (!err2) {
await act3()
}
}
await act4()

ซึ่งจริงๆสามารถแก้ได้ครับเป็น

let err1 = await act1()
if err1 return act4()
let err2 = await act2()
if err2 return act4()
let err3 = await act3()await act4()

สังเกตว่าอ่านง่ายกว่าเดิมเยอะเลยครับ หรือถ้าไม่ติดเรื่องต้องเช็ค Error ก็ปล่อยผ่านก็ได้ครับ จะได้ Error รัวๆ อยากเช็คก็เช็คทีเดียว

let err1 = await act1()
let err2 = await act2()
let err3 = await act3()
let err4 = await act4()

ขอบคุณครับ

--

--

Pagon GameDev
บริษัทต่างโลก อินเตอร์แอคทีฟ จำกัด

Interactive Developer ผู้สนใจการทำเกม และ Interactive XR VR MR AR มากว่า 10 ปี ปัจจุบันเป็น Co-Founder บริษัท ต่างโลก อินเตอร์แอคทีฟ จำกัด