เนื้อหาต่อไปนี้ เราจะมาดูในเรื่องของ Error Handler
หรือการจัดการ ควบคุม และดูแลความผิดพลาด หรือข้อผิดพลาดที่อาจจะ
เกิดขึ้นใน Express ซึ่งได้เคยเกริ่นเล็กน้อยไปแล้วในเนื้อหาเกี่ยวกับ
Middleware ฟังก์ชั่น ในบทความ ตามลิ้งค์ http://niik.in/910
ใน Express ก็มีการจัดการข้อผิพดลาด หรือ Error โดยใช้ Middleware ฟังก์ชั่น
ประเภท Error-handling middleware ซึ่งเป็น build-in ฟังก์ชั่นที่ถูกเพิ่มไปไว้ในส่วนท้ายสุดของ
middleware ฟังก์ชั่น อื่นๆ
การเกิด Error ใน Express
เรามาดูการทำงาน โดยใช้เนื้อหาต่อเนื่องจากตอนที่แล้ว http://niik.in/911
ที่เมื่อเราเปิดเข้ามาที่หน้าแรก path: "/" ตัว app ก็จะไปโหลดไฟล์ template พร้อมแสดงข้อมูลที่เรากำหนด
ซึ่งเป็นรูปแบบการทำงานปกติ ไฟล์ app.js เป็นดังนี้
const express = require('express') // ใช้งาน module express const app = express() // สร้างตัวแปร app เป็น instance ของ express const path = require('path') // เรียกใช้งาน path module const port = 3000 // port // ส่วนของการใช้งาน router module ต่างๆ const indexRouter = require('./routes/index') // view engine setup app.set('views', path.join(__dirname, 'views')); app.set('view engine', 'ejs'); app.set('view options', {delimiter: '?'}); app.use(express.json()) app.use(express.urlencoded({ extended: false })) app.use(express.static(path.join(__dirname, 'public'))) // เรียกใช้งาน indexRouter app.use('/', indexRouter) app.listen(port, function() { console.log(`Example app listening on port ${port}!`) })
ถ้าเราลองเข้าไปยัง path: "/user" ซึ่งไม่ได้มีการกำหนดการทำงานให้กับ route นี้ไว้ ดูว่าจะเกิดอะไรขึ้น
จะเห็นว่า มีการแจ้ง "Cannot GET /user" เป็น build-in การแสดง error เบื้องต้น ที่ทำงานอัตโนมัติใน Express
โดยที่เราไม่ได้แทรกโค้ดใดๆ เพิ่มเข้าไป นั่นหมายความว่า หลังจากผ่านคำสั่ง app.use() ที่ path ไม่ตรงเงื่อนไข
ก็จะมีการ ทำคำสั่งเกี่ยวกับการจัดการ error เกิดขึ้น เพื่อจะดูการดักจับการทำงาน ก่อนที่จะไปถึงการจัดการ error
อัตโนมัติ ให้เราแทรก คำสั่งนี้ เข้าไป จะได้ไฟล์ app.js บางส่วน เป็นดังนี้
// เรียกใช้งาน indexRouter app.use('/', indexRouter) // ทำงานทุก request ที่เข้ามา app.use(function(req, res, next) { throw new Error('BROKEN') // กำหนด error เอง })
ผลลัพธ์ที่ได้
คำว่า "BROKEN" คือ error message (err.message) ที่เรากำหนด และส่วนที่เหลือที่แสดง เรียกว่า error stack
(err.stack) ตัว error stack นี้จะแสดง ก็ต่อเมื่อ Express App ของเรามีการกำหนด environment variable หรือ
NODE_ENV เป็น "development" ซึ่งหมายถึงอยู่ในโหมดการพัฒนา แต่ถ้าเรากำหนด NODE_ENV เป็น "production"
ค่า error.stack จะไม่แสดง
เรามาลองดูค่าว่าตอนนี้ NODE_ENV ของเราเป็นค่าอะไร โดยใช้คำสั่ง
console.log(req.app.get('env'))
จะได้เป็นดังรูป ซึ่งขณะนี้ ค่า NODE_ENV เท่ากับ "development"
จากนั้นเราลองกำหนดค่า ให้เป็น "production" โดยใช้คำสั่ง
app.set('env','production')
จะได้ผลลัพธ์ดังรูป
ในส่วนของ console ฝั่ง server เราจะเห็นค่าต่างๆ แต่ในส่วนของ ผู้ใช้ จะไม่มีการแสดง err.stack ในโหมดนี้
รวมทั้งการแจ้ง error ก็เป็นการใช้งานจากตัว build-in ใช้สถานะเป็น 500 (Internal Server Error)
ให้เราแก้ไข "env" เป็น "development" เหมือนเดิม หรือปิดส่วนของการกำหนดค่า ไปก่อน เพื่อไปต่อในเนื้อหา
ตัวอย่างการเกิด error ข้างต้น เป็นแบบ synchronous ที่เมื่อเกิด error ขึ้น ตัว Express ก็จะดักจับ error นั้น
แล้วส่งต่อไปยังส่วนจัดการ error ที่เป็น build-in ในทันที
มาดูกรณีที่เป็น asynchronous ยกตัวอย่างการเกิด error
// ทำงานทุก request ที่เข้ามา app.use(function(req, res, next) { console.log(req.app.get('env')) // throw new Error('BROKEN') // กำหนด error เอง Promise.resolve().then(function () { throw new Error('BROKEN') }).catch(next) // error จถถูกส่งไปยัง error build-in ใน Express })
Promise Object เป็นลักษณะการทำงานแบบ asynchronous หรือก็คือเข้าใจอย่างง่าย ว่า สัญญาว่าจะทำสิ่งหนึ่ง
สิ่งใด แต่ไม่ได้ทำในทันทีขณะนั้น และเมื่อทำสิ่งหนึ่งสิ่งใดแล้ว จะแจ้งกลับมาอีกทีว่าสำเร็จหรือไม่
ในโค้ด เมื่อเกิด Promise.resolve() หมายถึงเมื่อได้ทำตามที่สั่งสำเร็จเรียบแล้วแล้ว ให้ทำคำสั่งใน then()
แล้วไปจบการทำงานใน คำสั่ง catch()
ในคำสั่งสุดท้าย asynchronous ก็ส่ง error ไปทำงานต่อ ผ่านฟังก์ชั่น next
จะเห็นว่า ไม่ว่าจะเป็นการเกิด error ในแบบ synchronous หรือ asynchronous ก็จะมีการส่งต่อ error นั้นที่เกิดขึ้น
ไปทำงานต่อ
กลับมาที่ปัญหาของเรา คือ ตอนนี้เมื่อ ผู้ใช้เรียกไปยัง path: "/user" ซึ่งเราไม่ได้กำหนดการทำงานให้กับ path นี้
หรือนั้นก็คือ Page Not Found เราน่าจะคุ้นกับคำนี้ สิ่งที่เราต้องการให้เกิดขึ้น หรือ error ที่เราต้องการส่งต่อไปก็คือ
404 page not found ดังนั้น เราจึงใช้ตัวช่วย สร้าง error object ผ่านการใช้งาน "http-errors" module ซึ่งเป็น Node
Module ที่เราสามารถเรียกใช้งานได้เลย ไม่ต้องติดตั้งเพิ่มเติม สามารถดูการใช้งานเพิ่มเติมได้ที่ http-errors
ให้เราเพิ่มการเรียกใช้ module เข้าไป
const createError = require('http-errors')
รูปแบบการใช้งาน
createError([status], [message], [properties])
ตัวอย่างเช่น
var err = createError(404, 'This video does not exist!')
ในที่นี้เราจะกำหนดแค่ status code เข้าไป
var err = createError(404)
แล้วส่งต่อค่าไปใน error build-in middleware ฟังก์ชั่น จะได้เป็น
// ทำงานทุก request ที่เข้ามา app.use(function(req, res, next) { var err = createError(404) next(err) })
จะได้ผลลัพธ์เป็นดังนี้
ตัว http-errors จะสร้าง error มีสถานะ err.status เป็น "404" มีข้อความเป็น err.message เป็น "Not Found"
ส่วนของข้อความ เราสามารถกำหนดคำเพิ่มไปเองก็ได้ ในที่นี้เราใช้ค่าเริ่มต้นที่ http-errors จัดให้ นอกจากนี้ในโหมด
"development" ยังมี err.stack เกิดขึ้นด้วย
จากขั้นตอนข้างต้น เมื่อเราดักจับ error แล้วสร้างรูปแบบ error ด้วย http-errors module และส่งต่อเข้ามาใน Express
ให้ตัว error-handling middleware ที่เป็น build-in ฟังก์ชั่น จัดการ ผลลัพธ์ ก็จะได้อย่างในรูปด้านบน หรือที่เรียกรูปแบบ
การจัดการดังกล่าวว่า default error handler
เราจะใช้งานการจัดการ error แบบกำหนดเอง กันในหัวข้อต่อไป
การจัดการ Error แบบกำหนดเอง
การสร้าง error-handling middleware ฟังก์ชั่นขึ้นมาเองนั้น มีรูปแบบคล้ายๆ กันกับ middleware ฟังก์ชั่นอื่นๆ
ยกเว้น middleware กรณีจัดการกับ error เราจะมี argument เพิ่มเข้ามาเป็น 4 ตัว จากปกติทั่วไปที่มีแค่ 3 นั่นก็คือ
มีตัว error ที่เราส่งเข้ามาจากคำสั่ง next ใน middleware ตัวก่อนหน้า
รูปแบบ error-handling middleware เบื้องต้น จะได้เป็น
// ส่วนจัดการ error app.use(function (err, req, res, next) { console.error(err.stack) console.error(err.message) console.error(err.status) res.status(500).send('Something broke!') })
โค้ดข้างต้น เรายังไม่ใช้ค่า error ที่ส่งมา ไปใช้งาน แต่เราทำการ ส่ง หน้า error status 500
และข้อความไปแสดงยังหน้า path: "/user" แทน ส่วน error ที่ถูกส่งมา เราลองแสดงใน console ฝั่ง
server เพื่อดูผลลัพธ์ จะได้เป็นดังนี้
จะเห็นค่าที่แสดงใน console.log() เป็นค่า error ที่เราส่งมา เป็นค่าที่ถูกต้อง ส่วนค่าที่เราส่งไปแสดงเป็นค่า
ทดสอบ ส่งค่าที่ต้องการไปแสดงยังหน้าผู้ใช้งาน เป็น 500 (Internal Server Error) แล้วเราก็ให้แสดงข้อความว่า
"Something broke!" ในหน้า error นั้น
ตอนนี้ เราเข้าใจหลักการทำงานของ ฟังก์ชั่น error-handling middleware แล้ว รู้ว่า error ที่ส่งมามีอะไรบ้าง
หรือกรณีไม่มีค่าส่งมา เราก็สามารถกำหนดค่าไปแสดงในหน้า error เป็นค่าอื่นๆ ตามต้องการได้
ประยุกต๋ใช้งาน Error ร่วมกับ Template Engine
มาถึงส่วนสุดท้าย เมื่อเรารู้จักการใช้งานการจัดการ error เบื่องต้นแล้ว เราก็มาประยุกต์ เพื่อแสดงหน้า error ใน
Template Engine โดยส่งค่า error ค่าจริงไปแสดง ซึ่งเราใช้ EJS
ให้เราสร้างไฟล์ error.ejs ด้วยรูปแบบโค้ดดังนี้เพิ่มเข้าไป
<!DOCTYPE html> <html> <head> <title><?= error.status ?> <?= message ?></title> <link rel='stylesheet' href='css/mycss.css' /> </head> <body> <h1><?= message ?></h1> <h2><?= error.status ?></h2> <pre><?= error.stack ?></pre> </body> </html>
แก้ไขส่วนจัดการ error เป็น
// ส่วนจัดการ error app.use(function (err, req, res, next) { // กำหนด response local variables res.locals.message = err.message res.locals.error = req.app.get('env') === 'development' ? err : {} // กำหนด status และ render หน้า error page res.status(err.status || 500) // ถ้ามี status หรือถ้าไม่มีใช้เป็น 500 res.render('error') })
การกำหนดตัวแปร ชื่อ message และ error แบบ response local เป็นการกำหนดขอบเขตของตัวแปร
ทั้งสองให้สามารถใช้งานได้ เฉพาะภายไนหน้า views ที่ render ในระหว่างที่เกิด request / response cycle
หรือก็คือสามารถใช้งานในไฟล์ error.ejs template
รูปแบบการกำหนดคือ
res.locals.[ชื่อตัวแปร] = [ค่าที่ต้องการ]
สำหรับค่าตัวแปร error ที่กำหนดด้วย res.locals.error นั้น มีการใส่เงื่อนไขเพิ่มเข้ามาคือ
ถ้า req.app.get('env') === 'development' หรือก็คืออยู่ในโหมด "development" ให้มีค่าเท่ากับ err object
ที่ส่งเข้ามา ซึ่งใน err object ก็จะมี err.message, err.status และ err.stack
แต่ถ้าไม่ได้อยู่ในโหมด "development" เช่น อยู่ในโหมด "production" ก็จะให้ค่า error เป็น object ว่างหรือค่าว่าง
res.locals.error = req.app.get('env') === 'development' ? err : {}
มาจากรูปแบบ
if( req.app.get('env') === 'development' ) { res.locals.error = err } else { res.locals.error = {} }
สำหรับการเรียกใช้งานตัวแปร response local variable ในไฟล์ template เราก็ใช้ชื่อตัวแปรนั้นๆ อ้างอิงได้เลย
เช่น message ใช้เป็น <?= message ?>
ส่วน error นั่นมี property ย่อย เราก็เรียกใช้งานในรุปแบบ object เป็น
<?= error.status ?> หรือ <?= error.stack ?> ลักษณะแบบนี้เป็นต้น
การกำหนดตัวแปรแบบ response local ทำให้เราไม่ต้องส่งค่าต่างๆ เหล่านี้เข้าไปไฟล์ template ตอนใช้คำสั่ง
render() โดยตรง นอกจากนั้น ทำให้เราสามารถจัดการ กำหนดรูปแบบหรือเงื่อนไขให้กับค่าต่างๆ เหล่านี้ได้สะดวกขึ้น
ไฟล์ app.js แบบเต็ม
const express = require('express') // ใช้งาน module express const app = express() // สร้างตัวแปร app เป็น instance ของ express const path = require('path') // เรียกใช้งาน path module const createError = require('http-errors') // เรียกใช้งาน http-errors module const port = 3000 // port // ส่วนของการใช้งาน router module ต่างๆ const indexRouter = require('./routes/index') // view engine setup app.set('views', path.join(__dirname, 'views')); app.set('view engine', 'ejs'); app.set('view options', {delimiter: '?'}); // app.set('env','production') app.use(express.json()) app.use(express.urlencoded({ extended: false })) app.use(express.static(path.join(__dirname, 'public'))) // เรียกใช้งาน indexRouter app.use('/', indexRouter) // ทำงานทุก request ที่เข้ามา app.use(function(req, res, next) { var err = createError(404) next(err) }) // ส่วนจัดการ error app.use(function (err, req, res, next) { // กำหนด response local variables res.locals.message = err.message res.locals.error = req.app.get('env') === 'development' ? err : {} // กำหนด status และ render หน้า error page res.status(err.status || 500) // ถ้ามี status หรือถ้าไม่มีใช้เป็น 500 res.render('error') }) app.listen(port, function() { console.log(`Example app listening on port ${port}!`) })
ดูผลลัพธ์และการทำงานในโหมด "development"
ทดสอบเปลี่ยนโหมดเป็น "production" จะเห็นว่าจะไม่แสดงในส่วนของ error status และ error stack
เนื้อหาในส่วนนี้ เราได้รู้จักกับ error ที่อาจจะเกิดขึ้นใน Express และการจัดการกับ error เบื้องต้น
เป็นแนวทางสำหรับทำความเข้าใจในเนื้อหาอื่นๆ ต่อไป