Handling Error Pada Ekspres JS - CRUDPRO

Handling Error Pada Ekspres JS

Menangani routes yang belum diproses

Misalkan pengguna mencoba mengakses endponint /api/tours (bukan /api/vi/tours). Responsnya terlihat seperti ini:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="utf-8">
  <title>Error</title>
</head>

<body>
  <pre>Cannot GET /api/tours/</pre>
</body>

</html>

Ini adalah API, jadi saya tidak ingin mengirim kembali HTML. Oleh karena itu, tentukan JSON yang akan dikirim kembali ke pengguna. Ini didefinisikan dalam file app.js.

// ROUTES
app.use('/api/v1/tours', tourRouter);
app.use('/api/v1/users', userRouter);

app.all('*', (req, res, next) => {
  res.status(404).json({
    status: 'fail',
    message: `Can't find ${req.originalUrl} on this server!`
  });
});

Semua routes yang diproses dapat berupa router tour atau router pengguna. routes lain diakhiri dengan fungsi app.all() yang ditentukan. Metode all() berisi semua jenis request, termasuk GET dan PATCH, dan tanda bintang menerima URL apa pun. Dari sana, tentukan middleware yang mengirimkan respons JSend.

Ikhtisar error Handling

errors dapat dibagi menjadi dua jenis: errors operasional dan errors pemrograman. errors pemrograman adalah bug yang diperkenalkan development ke dalam kode mereka. errors operasional, di sisi lain, tidak dapat dihindari ketika pengguna berinteraksi dengan aplikasi Anda. Ini termasuk jalur yang tidak valid, kegagalan koneksi server, dan input pengguna yang tidak valid. Anda perlu membuat middleware handling errors global untuk mempersiapkan errors ini sebelumnya.

Errors middleware

Untungnya, Express memiliki arsitektour yang memfasilitasi handling errors. Untuk mendefinisikan middleware handling errors, cukup definisikan middleware di app.js dengan empat argumen err, req, res, dan next. Selama empat argumen ini ada, Express akan mengenali middleware sebagai middleware handling errors. Paling pada dasarnya, terlihat seperti ini:

app.use((err, req, res, next) => {
  err.statusCode = err.statusCode || 500;
  err.status = err.status || 'error';

  res.status(err.statusCode).json({
    status: err.status,
    message: err.message
  });
});

Jika objek err masuk tidak memiliki statusCode atau status, tentukan nilai default. Kemudian kirim kembali objek JSON.

Kemudian buat errors baru di blok app.all() dari atas. Komentari kode saat ini, buat instance baru dari objek Error dengan pesan yang diteruskan, lalu tentukan beberapa properti.

app.all('*', (req, res, next) => {
  /*
  res.status(404).json({
    status: 'fail',
    message: `Can't find ${req.originalUrl} on this server!`
  });
  */
  const err = new Error(`Can't find ${req.originalUrl} on this server!`);
  err.status = 'fail';
  err.statusCode = 404;
  
  next(err);
});

Seperti biasa, panggil next() di akhir. Tapi kali ini, saya menggunakannya dengan cara baru. Jika Anda meneruskan argumen ke next(), fungsi mengasumsikan bahwa argumen tersebut adalah errors dan langsung menuju ke middleware handling errors.

Membuat kelas errors

Cara yang umum adalah mendapatkan objek Error asli dan memperluasnya dengan kelas Anda sendiri.

class AppError extends Error {
  constructor(message, statusCode) {
    super(message);

    this.statusCode = statusCode;
    this.status = `${statusCode}`.startsWith('4') ? 'fail' : 'error';
    this.isOperational = true;
    
    Error.captoureStackTrace(this, this.constructor);
  }
}

Fungsi super() hanya mengambil pesan sebagai argumen karena Error mengambilnya secara asli. Kemudian tambahkan properti statusCode dan status yang ditourunkan dari statusCode. Properti isOperational akan berguna nanti. Terakhir, baris captoureStackTrace mencegah kelas ini muncul di pelacakan tumpukan. Jejak tumpukan adalah bagian dari log konsol yang menunjukkan di mana errors terjadi dalam kode.

Setelah menempatkan kelas ini, Anda dapat menulis ulang blok app.all() sebagai berikut:

app.all('*', (req, res, next) => {
  next(new AppError(`Can't find ${req.originalUrl} on this server!`, 404));
});

Catching atau Menangkap errors dengan fungsi asinkron

Sejauh ini, saya telah menggunakan blok try/catch untuk menangkap errors dalam fungsi async/menunggu, tetapi kodenya sekarang terlihat berantakan. Untuk memperbaiki masalah ini, tempatkan blok try/catch di fungsi tingkat yang lebih tinggi, lalu tempatkan fungsi async/menunggu fungsi itu. Awal dari refactoring itu terlihat seperti ini:

const catchAsync = fn => {
  fn(req, res, next);
};

exports.createTour = catchAsync(async (req, res, next) => {
  try {
    const newTour = await Tour.create(req.body);

    res.status(201).json({
      status: 'success',
      data: { tour: newTour }
    });
  } catch (err) {
    res.status(400).json({
      status: 'fail',
      message: err
    });
  }
});

Jadi saya membungkus createTour dalam suatu fungsi dan kemudian meneruskannya sebagai argumen sehingga ketika terjadi errors, fungsi ini dapat menangani middleware handling errors. Seperti yang Anda ketahui, fungsi async /await function mengembalikan promises, tetapi jika tidak dapat diselesaikan, itu akan menimbulkan errors. Anda dapat menangkap errors menggunakan metode catch alih-alih blok try/catch. Ini memungkinkan Anda untuk menghapus blok try/catch dari createTour:

const catchAsync = fn => {
  fn(req, res, next).catch(err => next(err));
};

exports.createTour = catchAsync(async (req, res, next) => {
  const newTour = await Tour.create(req.body);

  res.status(201).json({
    status: 'success',
    data: { tour: newTour }
  });
});

Ada dua masalah dengan kode ini. Pertama, catchAsync tidak memiliki cara untuk mengetahui apa req, res, atau next. Kemudian, karena kita memanggil fn segera di catchAsync, createTour sama dengan hasil dari fungsi asinkron, bukan fungsi itu sendiri. Solusinya adalah mengembalikan fungsi ke catchAysnc. Kemudian fungsi itu akan ditugaskan ke createTour.

const catchAsync = fn => {
  retourn (req, res, next) => {
    fn(req, res, next).catch(err => next(err));
  };
};

Pemfaktoran ulang terakhir: next() secara otomatis dipanggil dengan parameter yang diterima oleh callback, jadi err => next (err) dapat disederhanakan menjadi next.

const catchAsync = fn => {
  retourn (req, res, next) => {
    fn(req, res, next).catch(next);
  };
};

Sekarang Anda siap untuk membungkus semua penangan dengan catchAsync.

Tambahkan Kode Status 404

Saat ini, endponint getTour mengembalikan errors jika Anda melewati ID yang tidak valid dan Tours yang sesuai jika Anda melewati ID yang sesuai yang valid. Namun, jika Anda melewati ID Mongo yang valid yang tidak ada di database, Anda akan mendapatkan respons dengan tour yang disetel ke null. Sebaliknya saya ingin melempar errors. Untuk melakukan ini, gunakan kelas AppError di dalam handler.

exports.getTour = catchAsync(async (req, res, next) => {
  const tour = await Tour.findById(req.params.id);

  if (!tour) {
    retourn next(new AppError('No tour found with that ID', 404));
  }

  res.status(200).json({
    status: 'success',
    data: { tour }
  });
});

Anda juga perlu menempelkan blok kode ini ke penangan yang diambil berdasarkan ID.

Errors Development dan Errors Produksi

Saya ingin mengirim pesan errors yang bersih kepada pengguna. Namun, pembangunan membutuhkan informasi sebanyak mungkin. Akses variabel lingkungan dan kembalikan respons yang sesuai.

const sendErrorDev = (err, res) => {
  res.status(err.statusCode).json({
    status: err.status,
    error: err,
    message: err.message,
    stack: err.stack
  });
};

const sendErrorProd = (err, res) => {
  res.status(err.statusCode).json({
    status: err.status,
    message: err.message
  });
};

module.exports = (err, req, res, next) => {
  err.statusCode = err.statusCode || 500;
  err.status = err.status || 'error';

  if (process.env.NODE_ENV === 'development') {
    sendErrorDev(err, res);
  } else if (process.env.NODE_ENV === 'production') {
    sendErrorProd(err, res);
  }
};

errors operasi dijelaskan di bawah ini. errors yang ditentukan oleh development adalah errors operasional. Hanya mengirim pesan errors klien dengan errors operasional, bukan errors pemrograman. Kami menentukan properti isOperational di kelas AppError di atas, sehingga kami dapat menggunakannya di sini.

const sendErrorProd = (err, res) => {
  // Operational, trusted error: send message to client
  if (err.isOperational) {
    res.status(err.statusCode).json({
      status: err.status,
      message: err.message
    });

    // Programming or other unknown error
  } else {
    // 1) Log error
    console.error('ERROR 💥: ', err);

    // 2) Send generic message
    res.status(500).json({
      status: 'error',
      message: 'Something went very wrong!'
    });
  }
};

errors Mongoose

Mongoose mungkin mengirim errors yang bukan merupakan tourunan dari kelas

AppError

, tetapi masih ingin mengirimkannya ke klien. errors ini dapat menjadi salah satu dari tiga jenis. Pertama, request dengan ID Mongo yang tidak valid (misalnya "potato"). Selanjutnya, errors kunci duplikat saat membuat dokumen (misalnya, dua tour dengan nama yang sama). Yang ketiga adalah errors validasi lainnya (misalnya, tour dengan peringkat rata-rata 2.000).

Tipe pertama disebut cast error. errors cast menunjukkan bahwa input dalam format yang salah. Properti nama diatour ke "CastError". Anda dapat mengakses properti ini di penangan errors global.

module.exports = (err, req, res, next) => {
  err.statusCode = err.statusCode || 500;
  err.status = err.status || 'error';

  if (process.env.NODE_ENV === 'development') {
    sendErrorDev(err, res);
  } else if (process.env.NODE_ENV === 'production') {
    let error = { ...err };
    if (error.name === 'CastError') {
      error = handleCastErrorDB(error);
    }
    sendErrorProd(error, res);
  }
};

Ini adalah praktik yang buruk untuk mengubah argumen yang masuk, jadi tidak sengaja membuat salinan dan menggunakannya sebagai gantinya. Fungsi handleCastErrorDB() menggunakan kelas AppError sebagai berikut:

const handleCastErrorDB = err => {
  const message = `Invalid ${err.path}: ${err.value}`;
  retourn new AppError(message, 400);
};

Kemudian tangani errors field duplikat. errors ini tidak memiliki nama yang unik, jadi gunakan kode errors untuk mengakses errors.

let error = { ...err };
    if (error.name === 'CastError') error = handleCastErrorDB(error);
    if (error.code === 11000) error = handleDuplicateFieldsDB(error);

Kemudian pawang terlihat seperti ini: Gunakan ekspresi reguler yang menargetkan teks di antara tanda kutip.

const handleDuplicateFieldsDB = err => {
  const value = err.errmsg.match(/(["'])(?:(?=(\\?))\2.)*?\1/)[0];
  const message = `Duplicate field value: ${value}. Please use anothe value!`;
  retourn new AppError(message, 400);
};

Terakhir, tangani errors validasi. Untungnya, mereka memiliki properti nama.

let error = { ...err };
    if (error.name === 'CastError') error = handleCastErrorDB(error);
    if (error.code === 11000) error = handleDuplicateFieldsDB(error);
    if (error.name === 'ValidationError')
      error = handleValidationErrorDB(error);

errors ini memiliki objek errors dengan objek untuk setiap field yang salah. Gunakan Object.values() untuk mengulang setiap objek yang salah.

const handleValidationErrorDB = err => {
  const errors = Object.values(err.errors).map(el => el.message);

  const message = `Invalid input data. ${errors.join('. ')}`;
  retourn new AppError(message, 400);
};

Penolakan yang belum diproses

Anda mungkin mendapatkan errors yang tidak mencapai aplikasi Express Anda dan tidak dapat ditangani oleh penangan errors global. Salah satu contohnya adalah upaya untuk mengakses database dengan password yang tidak valid. Ini menghasilkan penolakan yang tidak diproses. errors ini dapat ditangani dalam file server. Letakkan kode ini di bagian bawah sehingga berfungsi sebagai jaring pengaman untuk semua yang tidak dapat ditangkap oleh programmer.

process.on('unhandledRejection', err => {
  console.log(err.name, err.message);
  console.log('UNHANDLED REJECTION! 💥 Shutting down...');
  process.exit(1);
});

1 singkatan dari "uncaught exception" dan 0 singkatan dari "success". Masalahnya adalah process.exit() adalah cara yang sangat mendadak untuk keluar dari server, membunuh semua request yang tertunda. Tutup server terlebih dahulu, lalu jalankan process.exit().

Pengecualian yang tidak tertangkap

Pengecualian yang tidak tertangkap mirip dengan penolakan yang tidak ditangani, tetapi mereka adalah kode sinkronisasi dan tidak ada hubungannya dengan janji. Contoh sederhana dari pengecualian yang tidak tertangkap adalah ketika Anda mencoba untuk konsol log x ketika Anda belum pernah mendefinisikan x. Menangani pengecualian yang tidak tertangkap sangat mirip dengan menangani penolakan yang tidak ditangani. Perbedaan utama adalah bahwa ini bukan kode asinkron, sehingga fungsi panggilan balik tidak lagi diperlukan. Selain itu, kami tidak mendeteksi bug apa pun yang terjadi sebelum mendefinisikan metode ini, jadi kami perlu meletakkan blok kode ini di awal file sebelum kami membutuhkan aplikasi.

/* eslint-disable no-console */
const mongoose = require('mongoose');
// eslint-disable-next-line import/newline-after-import
const dotenv = require('dotenv');
dotenv.config({ path: './config.env' });

process.on('uncaughtException', err => {
  console.log(err.name, err.message);
  console.log('UNCAUGHT EXCEPTION! 💥 Shutting down...');

  process.exit(1);

});

const app = require('./app');

ringkasan

handling errors di Express dan Mongoose jauh lebih dalam dari ini. Misalnya, Anda dapat menentukan errors dengan tingkat keparahan yang berbeda. Dapat memicu errors untuk mengirim email ke administrator. Namun untuk saat ini, ini adalah dasar yang kuat untuk membangun aplikasi hebat menggunakan Node.js.