Node js express response error

Error Handling refers to how Express catches and processes errors that occur both synchronously and asynchronously. Express comes with a default error handler so you don’t need to write your own to get started.

Error Handling refers to how Express catches and processes errors that
occur both synchronously and asynchronously. Express comes with a default error
handler so you don’t need to write your own to get started.

Catching Errors

It’s important to ensure that Express catches all errors that occur while
running route handlers and middleware.

Errors that occur in synchronous code inside route handlers and middleware
require no extra work. If synchronous code throws an error, then Express will
catch and process it. For example:

app.get('/', (req, res) => {
  throw new Error('BROKEN') // Express will catch this on its own.
})

For errors returned from asynchronous functions invoked by route handlers
and middleware, you must pass them to the next() function, where Express will
catch and process them. For example:

app.get('/', (req, res, next) => {
  fs.readFile('/file-does-not-exist', (err, data) => {
    if (err) {
      next(err) // Pass errors to Express.
    } else {
      res.send(data)
    }
  })
})

Starting with Express 5, route handlers and middleware that return a Promise
will call next(value) automatically when they reject or throw an error.
For example:

app.get('/user/:id', async (req, res, next) => {
  const user = await getUserById(req.params.id)
  res.send(user)
})

If getUserById throws an error or rejects, next will be called with either
the thrown error or the rejected value. If no rejected value is provided, next
will be called with a default Error object provided by the Express router.

If you pass anything to the next() function (except the string 'route'),
Express regards the current request as being an error and will skip any
remaining non-error handling routing and middleware functions.

If the callback in a sequence provides no data, only errors, you can simplify
this code as follows:

app.get('/', [
  function (req, res, next) {
    fs.writeFile('/inaccessible-path', 'data', next)
  },
  function (req, res) {
    res.send('OK')
  }
])

In the above example next is provided as the callback for fs.writeFile,
which is called with or without errors. If there is no error the second
handler is executed, otherwise Express catches and processes the error.

You must catch errors that occur in asynchronous code invoked by route handlers or
middleware and pass them to Express for processing. For example:

app.get('/', (req, res, next) => {
  setTimeout(() => {
    try {
      throw new Error('BROKEN')
    } catch (err) {
      next(err)
    }
  }, 100)
})

The above example uses a try...catch block to catch errors in the
asynchronous code and pass them to Express. If the try...catch
block were omitted, Express would not catch the error since it is not part of the synchronous
handler code.

Use promises to avoid the overhead of the try...catch block or when using functions
that return promises. For example:

app.get('/', (req, res, next) => {
  Promise.resolve().then(() => {
    throw new Error('BROKEN')
  }).catch(next) // Errors will be passed to Express.
})

Since promises automatically catch both synchronous errors and rejected promises,
you can simply provide next as the final catch handler and Express will catch errors,
because the catch handler is given the error as the first argument.

You could also use a chain of handlers to rely on synchronous error
catching, by reducing the asynchronous code to something trivial. For example:

app.get('/', [
  function (req, res, next) {
    fs.readFile('/maybe-valid-file', 'utf-8', (err, data) => {
      res.locals.data = data
      next(err)
    })
  },
  function (req, res) {
    res.locals.data = res.locals.data.split(',')[1]
    res.send(res.locals.data)
  }
])

The above example has a couple of trivial statements from the readFile
call. If readFile causes an error, then it passes the error to Express, otherwise you
quickly return to the world of synchronous error handling in the next handler
in the chain. Then, the example above tries to process the data. If this fails then the
synchronous error handler will catch it. If you had done this processing inside
the readFile callback then the application might exit and the Express error
handlers would not run.

Whichever method you use, if you want Express error handlers to be called in and the
application to survive, you must ensure that Express receives the error.

The default error handler

Express comes with a built-in error handler that takes care of any errors that might be encountered in the app. This default error-handling middleware function is added at the end of the middleware function stack.

If you pass an error to next() and you do not handle it in a custom error
handler, it will be handled by the built-in error handler; the error will be
written to the client with the stack trace. The stack trace is not included
in the production environment.

Set the environment variable NODE_ENV to production, to run the app in production mode.

When an error is written, the following information is added to the
response:

  • The res.statusCode is set from err.status (or err.statusCode). If
    this value is outside the 4xx or 5xx range, it will be set to 500.
  • The res.statusMessage is set according to the status code.
  • The body will be the HTML of the status code message when in production
    environment, otherwise will be err.stack.
  • Any headers specified in an err.headers object.

If you call next() with an error after you have started writing the
response (for example, if you encounter an error while streaming the
response to the client) the Express default error handler closes the
connection and fails the request.

So when you add a custom error handler, you must delegate to
the default Express error handler, when the headers
have already been sent to the client:

function errorHandler (err, req, res, next) {
  if (res.headersSent) {
    return next(err)
  }
  res.status(500)
  res.render('error', { error: err })
}

Note that the default error handler can get triggered if you call next() with an error
in your code more than once, even if custom error handling middleware is in place.

Writing error handlers

Define error-handling middleware functions in the same way as other middleware functions,
except error-handling functions have four arguments instead of three:
(err, req, res, next). For example:

app.use((err, req, res, next) => {
  console.error(err.stack)
  res.status(500).send('Something broke!')
})

You define error-handling middleware last, after other app.use() and routes calls; for example:

const bodyParser = require('body-parser')
const methodOverride = require('method-override')

app.use(bodyParser.urlencoded({
  extended: true
}))
app.use(bodyParser.json())
app.use(methodOverride())
app.use((err, req, res, next) => {
  // logic
})

Responses from within a middleware function can be in any format, such as an HTML error page, a simple message, or a JSON string.

For organizational (and higher-level framework) purposes, you can define
several error-handling middleware functions, much as you would with
regular middleware functions. For example, to define an error-handler
for requests made by using XHR and those without:

const bodyParser = require('body-parser')
const methodOverride = require('method-override')

app.use(bodyParser.urlencoded({
  extended: true
}))
app.use(bodyParser.json())
app.use(methodOverride())
app.use(logErrors)
app.use(clientErrorHandler)
app.use(errorHandler)

In this example, the generic logErrors might write request and
error information to stderr, for example:

function logErrors (err, req, res, next) {
  console.error(err.stack)
  next(err)
}

Also in this example, clientErrorHandler is defined as follows; in this case, the error is explicitly passed along to the next one.

Notice that when not calling “next” in an error-handling function, you are responsible for writing (and ending) the response. Otherwise those requests will “hang” and will not be eligible for garbage collection.

function clientErrorHandler (err, req, res, next) {
  if (req.xhr) {
    res.status(500).send({ error: 'Something failed!' })
  } else {
    next(err)
  }
}

Implement the “catch-all” errorHandler function as follows (for example):

function errorHandler (err, req, res, next) {
  res.status(500)
  res.render('error', { error: err })
}

If you have a route handler with multiple callback functions you can use the route parameter to skip to the next route handler. For example:

app.get('/a_route_behind_paywall',
  (req, res, next) => {
    if (!req.user.hasPaid) {
      // continue handling this request
      next('route')
    } else {
      next()
    }
  }, (req, res, next) => {
    PaidContent.find((err, doc) => {
      if (err) return next(err)
      res.json(doc)
    })
  })

In this example, the getPaidContent handler will be skipped but any remaining handlers in app for /a_route_behind_paywall would continue to be executed.

Calls to next() and next(err) indicate that the current handler is complete and in what state. next(err) will skip all remaining handlers in the chain except for those that are set up to handle errors as described above.

Обработка ошибок

Функции промежуточного обработчика для обработки ошибок определяются так же, как и другие функции промежуточной обработки, но с указанием для функции обработки ошибок не трех, а четырех аргументов: (err, req, res, next). Например:


app.use(function(err, req, res, next) {
  console.error(err.stack);
  res.status(500).send('Something broke!');
});

Промежуточный обработчик для обработки ошибок должен быть определен последним, после указания всех app.use() и вызовов маршрутов; например:


var bodyParser = require('body-parser');
var methodOverride = require('method-override');

app.use(bodyParser());
app.use(methodOverride());
app.use(function(err, req, res, next) {
  // logic
});

Ответы, поступающие из функции промежуточной обработки, могут иметь любой формат, в зависимости от ваших предпочтений. Например, это может быть страница сообщения об ошибке HTML, простое сообщение или строка JSON.

В целях упорядочения (и для фреймворков более высокого уровня) можно определить несколько функций промежуточной обработки ошибок, точно так же, как это допускается для обычных функций промежуточной обработки. Например, для того чтобы определить обработчик ошибок для запросов, совершаемых с помощью XHR, и для остальных запросов, можно воспользоваться следующими командами:


var bodyParser = require('body-parser');
var methodOverride = require('method-override');

app.use(bodyParser());
app.use(methodOverride());
app.use(logErrors);
app.use(clientErrorHandler);
app.use(errorHandler);

В данном примере базовый код logErrors может записывать информацию о запросах и ошибках в stderr, например:


function logErrors(err, req, res, next) {
  console.error(err.stack);
  next(err);
}

Кроме того, в данном примере clientErrorHandler определен, как указано ниже; в таком случае ошибка явным образом передается далее следующему обработчику:


function clientErrorHandler(err, req, res, next) {
  if (req.xhr) {
    res.status(500).send({ error: 'Something failed!' });
  } else {
    next(err);
  }
}

“Обобщающая” функция errorHandler может быть реализована так:


function errorHandler(err, req, res, next) {
  res.status(500);
  res.render('error', { error: err });
}

При передаче какого-либо объекта в функцию next() (кроме строки 'route'), Express интерпретирует текущий запрос как ошибку и пропустит все остальные функции маршрутизации и промежуточной обработки, не являющиеся функциями обработки ошибок. Для того чтобы обработать данную ошибку определенным образом, необходимо создать маршрут обработки ошибок, как описано в следующем разделе.

Если задан обработчик ошибок с несколькими функциями обратного вызова, можно воспользоваться параметром route, чтобы перейти к следующему обработчику маршрута. Например:


app.get('/a_route_behind_paywall',
  function checkIfPaidSubscriber(req, res, next) {
    if(!req.user.hasPaid) {

      // continue handling this request
      next('route');
    }
  }, function getPaidContent(req, res, next) {
    PaidContent.find(function(err, doc) {
      if(err) return next(err);
      res.json(doc);
    });
  });

В данном примере обработчик getPaidContent будет пропущен, но выполнение всех остальных обработчиков в app для /a_route_behind_paywall будет продолжено.

Вызовы next() и next(err) указывают на завершение выполнения текущего обработчика и на его состояние. next(err) пропускает все остальные обработчики в цепочке, кроме заданных для обработки ошибок, как описано выше.

Стандартный обработчик ошибок

В Express предусмотрен встроенный обработчик ошибок, который обрабатывает любые возможные ошибки, встречающиеся в приложении. Этот стандартный обработчик ошибок добавляется в конец стека функций промежуточной обработки.

В случае передачи ошибки в next() без обработки с помощью обработчика ошибок, такая ошибка будет обработана встроенным обработчиком ошибок. Ошибка будет записана на клиенте с помощью трассировки стека. Трассировка стека не включена в рабочую среду.

Для запуска приложения в рабочем режиме необходимо задать для переменной среды NODE_ENV значение production.

При вызове next() с ошибкой после начала записи ответа
(например, если ошибка обнаружена во время включения ответа в поток, направляемый клиенту), стандартный обработчик ошибок Express закрывает соединение и отклоняет запрос.

Поэтому при добавлении нестандартного обработчика ошибок вам потребуется делегирование в стандартные
механизмы обработки ошибок в Express в случае, если заголовки уже были отправлены клиенту:


function errorHandler(err, req, res, next) {
  if (res.headersSent) {
    return next(err);
  }
  res.status(500);
  res.render('error', { error: err });
}

Когда я только начинал работать с Express и пытался разобраться с тем, как обрабатывать ошибки, мне пришлось нелегко. Возникало такое ощущение, будто никто не писал о том, что мне было нужно. В итоге мне пришлось самому искать ответы на мои вопросы. Сегодня я хочу рассказать всё, что знаю об обработке ошибок в Express-приложениях. Начнём с синхронных ошибок.

Обработка синхронных ошибок

Если вам нужно обработать синхронную ошибку, то вы можете, для начала, с помощью инструкции throw, выдать такую ошибку в обработчике запроса Express. Обратите внимание на то, что обработчики запросов ещё называют «контроллерами», но я предпочитаю использовать термин «обработчик запросов» так как он кажется мне понятнее.

Вот как это выглядит:

app.post('/testing', (req, res) => {
  throw new Error('Something broke! ')
})

Такие ошибки можно перехватить с помощью обработчика ошибок Express. Если вы не написали собственный обработчик ошибок (подробнее об этом мы поговорим ниже), то Express обработает ошибку с помощью обработчика, используемого по умолчанию.

Вот что делает стандартный обработчик ошибок Express:

  1. Устанавливает код состояния HTTP-ответа в значение 500.
  2. Отправляет сущности, выполнившей запрос, текстовый ответ.
  3. Логирует текстовый ответ в консоль.

Сообщение об ошибке, выведенное в консоль

Обработка асинхронных ошибок

Для обработки асинхронных ошибок нужно отправить ошибку обработчику ошибок Express через аргумент next:

app.post('/testing', async (req, res, next) => {
  return next(new Error('Something broke again! '))
})

Вот что попадёт в консоль при логировании этой ошибки.

Сообщение об ошибке, выведенное в консоль

Если вы пользуетесь в Express-приложении конструкцией async/await, то вам понадобится использовать функцию-обёртку, наподобие express-async-handler. Это позволяет писать асинхронный код без блоков try/catch. Подробнее об async/await в Express можно почитать здесь.

const asyncHandler = require('express-async-handler')

app.post('/testing', asyncHandler(async (req, res, next) => {
  // Сделать что-нибудь
}))

После того, как обработчик запроса обёрнут в express-async-handler, то можно, так же, как было описано выше, выбросить ошибку с использованием инструкции throw. Эта ошибка попадёт к обработчику ошибок Express.

app.post('/testing', asyncHandler(async (req, res, next) => {
  throw new Error('Something broke yet again! ')
}))

Сообщение об ошибке, выведенное в консоль

Написание собственного обработчика ошибок

Обработчики ошибок Express принимают 4 аргумента:

  1. error
  2. req
  3. res
  4. next

Размещать их нужно после промежуточных обработчиков и маршрутов.

app.use(/*...*/)
app.get(/*...*/)
app.post(/*...*/)
app.put(/*...*/)
app.delete(/*...*/)

// Собственный обработчик ошибок нужно поместить после всех остальных промежуточных обработчиков
app.use((error, req, res, next) => { /* ... */ })

Если создать собственный обработчик ошибок, то Express прекратит использование стандартного обработчика. Для того чтобы обработать ошибку, нужно сформировать ответ для фронтенд-приложения, которое обратилось к конечной точке, в которой возникла ошибка. Это означает, что нужно выполнить следующие действия:

  1. Сформировать и отправить подходящий код состояния ответа.
  2. Сформировать и отправить подходящий ответ.

То, какой именно код состояния подойдёт в каждом конкретном случае, зависит от того, что именно произошло. Вот список типичных ошибок, к обработке которых вы должны быть готовы:

  1. Ошибка 400 Bad Request. Используется в двух ситуациях. Во-первых — тогда, когда пользователь не включил в запрос необходимое поле (например — в отправленной платёжной форме не заполнено поле со сведениями о кредитной карте). Во-вторых — тогда, когда в запросе содержатся некорректные данные (например — ввод в поле пароля и в поле подтверждения пароля разных паролей).
  2. Ошибка 401 Unauthorized. Этот код состояния ответа применяется в том случае, если пользователь ввёл неправильные учётные данные (вроде имени пользователя, адреса электронной почты или пароля).
  3. Ошибка 403 Forbidden. Используется в тех случаях, когда пользователю не разрешён доступ к конечной точке.
  4. Ошибка 404 Not Found. Применяется в тех случаях, когда конечную точку невозможно обнаружить.
  5. Ошибка 500 Internal Server Error. Применяется тогда, когда запрос, отправленный фронтендом, сформирован правильно, но на бэкенде при этом возникла какая-то ошибка.

После того, как определён подходящий код состояния ответа, его нужно установить с помощью res.status:

app.use((error, req, res, next) => {
  // Ошибка, выдаваемая в ответ на неправильно сформированный запрос
  res.status(400)
  res.json(/* ... */)
})

Код состояния ответа должен соответствовать сообщению об ошибке. Для этого нужно отправлять код состояния вместе с ошибкой.

Легче всего это сделать с помощью пакета http-errors. Он позволяет отправлять в ошибке три фрагмента информации:

  1. Код состояния ответа.
  2. Сообщение, сопутствующее ошибке.
  3. Любые данные, которые нужно отправить (это необязательно).

Вот как установить пакет http-errors:

npm install http-errors --save

Вот как этим пакетом пользоваться:

const createError = require('http-errors')

// Создание ошибки
throw createError(status, message, properties)

Рассмотрим пример, который позволит как следует в этом всём разобраться.

Представим, что мы пытаемся обнаружить пользователя по адресу его электронной почты. Но этого пользователя найти не удаётся. В результате мы решаем отправить в ответ на соответствующий запрос ошибку User not found, сообщающую вызывающей стороне о том, что пользователь не найден.

Вот что нам нужно будет сделать при создании ошибки:

  1. Установить код состояния ответа как 400 Bad Request (ведь пользователь ввёл неправильные данные). Это будет наш первый параметр.
  2. Отправить вызывающей стороне сообщение наподобие User not found. Это будет второй параметр.

app.put('/testing', asyncHandler(async (req, res) => {
  const { email } = req.body
  const user = await User.findOne({ email })

  // Если пользователь не найден - выбросим ошибку
  if (!user) throw createError(400, `User '${email}' not found`)
}))

Получить код состояния можно с помощью конструкции error.status, а сообщение ошибки — с помощью error.message:

// Логирование ошибки
app.use((error, req, res, next) => {
  console.log('Error status: ', error.status)
  console.log('Message: ', error.message)
})

Результат логирования ошибки в консоли

Затем состояние ответа устанавливают с помощью res.status, а сообщение записывают в res.json:

app.use((error, req, res, next) => {
  // Установка кода состояния ответа
  res.status(error.status)

  // Отправка ответа
  res.json({ message: error.message })
})

Лично я предпочитаю отправлять в подобных ответах код состояния, сообщение и результат трассировки стека. Это облегчает отладку.

app.use((error, req, res, next) => {
  // Установка кода состояния ответа
  res.status(error.status)

  // Отправка ответа
  res.json({
    status: error.status,
    message: error.message,
    stack: error.stack
  })
})

▍Код состояния ответа, используемый по умолчанию

Если источником ошибки не является createError, то у неё не будет свойства status. Вот пример, в котором сделана попытка прочесть несуществующий файл с помощью fs.readFile:

const fs = require('fs')
const util = require('util')

// Преобразуем readFile из функции, использующей коллбэки, в async/await-функцию.
// Подробности об этом смотрите здесь: https://zellwk.com/blog/callbacks-to-promises
const readFilePromise = util.promisify(fs.readFile)

app.get('/testing', asyncHandler(async (req, res, next) => {
  const data = await readFilePromise('some-file')
})

У такого объекта ошибки не будет свойства status:

app.use((error, req, res, next) => {
  console.log('Error status: ', error.status)
  console.log('Message: ', error.message)
})

Результат логирования ошибки в консоли

В подобных случаях можно задать код ошибки, используемый по умолчанию. А именно, речь идёт об ошибке 500 Internal Server Error:

app.use((error, req, res, next) => {
  res.status(error.status || 500)
  res.json({
    status: error.status,
    message: error.message,
    stack: error.stack
  })
})

▍Изменение кода состояния ошибки

Предположим, мы собираемся прочитать некий файл, воспользовавшись данными, предоставленными пользователем. Если такого файла не существует, это значит, что нам нужно выдать ошибку 400 Bad Request. Ведь в том, что файл найти не удаётся, нет вины сервера.

В подобном случае нужно воспользоваться конструкцией try/catch для перехвата исходной ошибки. Затем нужно воссоздать объект ошибки с помощью createError:

app.get('/testing', asyncHandler(async (req, res, next) => {
  try {
    const { file } = req.body
    const contents = await readFilePromise(path.join(__dirname, file))
  } catch (error) {
    throw createError(400, `File ${file} does not exist`)
  }
})

▍Обработка ошибок 404

Если запрос прошёл через все промежуточные обработчики и маршруты, но так и не был обработан, это означает, что конечная точка, соответствующая такому запросу, не была найдена.

Для обработки ошибок 404 Not Found нужно добавить, между маршрутами и обработчиком ошибок, дополнительный обработчик. Вот как выглядит создание объекта ошибки 404:

// Промежуточные обработчики...
// Маршруты...

app.use((req, res, next) => {
  next(createError(404))
})

// Обработчик ошибок...

Сведения об ошибке

▍Замечания об ошибке ERR_HTTP_HEADERS_SENT

Не впадайте в панику если видите сообщение об ошибке ERR_HTTP_HEADERS_SENT: Cannot set headers after they are sent to the client. Она возникает из-за того, что в одном и том же обработчике многократно вызывается метод, устанавливающий заголовки ответа. Вот методы, вызов которых приводит к автоматической установке заголовков ответа:

  1. res.send
  2. res.json
  3. res.render
  4. res.sendFile
  5. res.sendStatus
  6. res.end
  7. res.redirect

Так, например, если вы вызовете методы res.render и res.json в одном и том же обработчике ответа, то вы получите ошибку ERR_HTTP_HEADERS_SENT:

app.get('/testing', (req, res) => {
  res.render('new-page')
  res.json({ message: '¯_(ツ)_/¯' })
})

В результате, в том случае, если вы сталкиваетесь с этой ошибкой, тщательно проверьте код обработчиков ответа и убедитесь в том, что в нём нет ситуаций, в которых вызывается несколько вышеописанных методов.

▍Обработка ошибок и потоковая передача данных

Если что-то идёт не так при потоковой передаче ответа фронтенду, то можно столкнуться с той же самой ошибкой ERR_HTTP_HEADERS_SENT.

В подобном случае обработку ошибок нужно передать стандартным обработчикам. Такой обработчик отправит ошибку и автоматически закроет соединение.

app.use((error, req, res, next) => {
  // Сделать это нужно только в том случае, если ответ передаётся в потоковом режиме
  if (res.headersSent) {
    return next(error)
  }

  // Остальной код обработки ошибок
})

Итоги

Сегодня я рассказал вам всё, что знаю об обработке ошибок в Express. Надеюсь, это поможет вам писать более надёжные Express-приложения.

Уважаемые читатели! Как вы обрабатываете ошибки в своих Node.js-проектах?

Error handling functions in an application detect and capture multiple error conditions and take appropriate remedial actions to either recover from those errors or fail gracefully. Common examples of remedial actions are providing a helpful message as output, logging a message in an error log that can be used for diagnosis, or retrying the failed operation.

Express is a framework for developing a web application in Node.js. In an earlier article we had introduced the Express framework with examples of using its powerful features which was followed by a second article on middleware functions in Express. In both of those articles, we briefly explained error handling using middleware functions.

This is the third article in the Express series where we will focus on handling errors in Node.js applications written using Express and understand the below concepts:

  1. Handling errors with the default error handler provided by Express.
  2. Creating custom error handlers to override the default error handling behavior.
  3. Handling errors thrown by asynchronous functions invoked in the routes defined in the Express application.
  4. Handling errors by chaining error-handling middleware functions.

Example Code

This article is accompanied by a working code example on GitHub.

Prerequisites

A basic understanding of Node.js and components of the Express framework is advisable.

Please refer to our earlier article for an introduction to Express.

Basic Setup for Running the Examples

We need to first set up a Node.js project for running our examples of handling errors in Express applications. Let us create a folder and initialize a Node.js project under it by running the npm init command:

mkdir storefront
cd storefront
npm init -y

Running these commands will create a Node.js project containing a package.json file.

We will next install the Express framework using the npm install command as shown below:

npm install express --save

When we run this command, it will install the Express framework and also add it as a dependency in our package.json file.

We will now create a file named index.js under a folder: js and open the project folder in our favorite code editor. We are using Visual Studio Code as our source-code editor.

Let us now add the following lines of code to index.js for running a simple HTTP server:

const express = require('express');

const app = express();

// Route for handling get request for path /
app.get('/', (request, response) => {
    response.send('response for GET request');
})

// Route for handling post request for path /products
app.post('/products', (request, response) => {
  ...
  response.json(...)
})

// start the server
app.listen(3000, 
   () => console.log('Server listening on port 3000.'))

In this code snippet, we are importing the express module and then calling the listen() function on the app handle to start our server.

We have also defined two routes that will accept the requests at URLs: / and /products. For an elaborate explanation of routes and handler functions, please refer to our earlier article for an introduction to Express.

We can run our application with the node command:

This will start a server that will listen for requests in port 3000.

We have also defined a server application in a file: js/server.js which we can run to simulate an external service. We can run the server application with the command:

This will start the server application on port 3001 where we can access a REST API on a URL: http://localhost:3001/products. We will call this service in some of our examples to test errors related to an external API call.

The application in index.js does not contain any error handling code as yet. Node.js applications crash when they encounter unhandled exceptions. So we will next add code to this application for simulating different error conditions and handling them in the subsequent sections.

Handling Errors in Route Handler Functions

The simplest way of handling errors in Express applications is by putting the error handling logic in the individual route handler functions. We can either check for specific error conditions or use a try-catch block for intercepting the error condition before invoking the logic for handling the error.

Examples of error handling logic could be logging the error stack to a log file or returning a helpful error response.

An example of a error handling in a route handler function is shown here:

const express = require('express')
const app = express()

app.use('/products', express.json({ limit: 100 }))

// handle post request for path /products
app.post('/products', (request, response) => {
  const name = request.body.name                
  ...
  ...

  // Check for error condition
  if(name == null){
    // Error handling logic: log the error
    console.log("input error")

    // Error handling logic: return error response
    response
      .status(400)
      .json({ message: "Mandatory field: name is missing. " })
  }else{
    // continue with normal processing             
    const productCreationResponse = { result: "success"}

    // return success response
    response.json(productCreationResponse)
  }
})

Here we are checking for the error condition by checking for the presence of a mandatory input in the request payload and returning the error as an HTTP error response with error code 400 and an error message as part of the error handling logic.

Here is one more example of handling error using a try-catch block:

const express = require('express')
const axios = require("axios")
const app = express()

app.get('/products', async (request, response) => {
  try{
    const apiResponse = await axios.get("http://localhost:3001/products")

    const jsonResponse = apiResponse.data
    console.log("response " + jsonResponse)
    
    response.send(jsonResponse)
  } catch(error) { // intercept the error in catch block

    // return error response
    response
        .status(500)
        .json({ message: "Error in invocation of API: /products" })
  }

})

Here also we are handling the error in the route handler function. We are intercepting the error in a catch block and returning an error message with an error code of 500 in the HTTP response.

But this method of putting error handling logic in all the route handler functions is not clean. We will try to handle this more elegantly using the middleware functions of Express as explained in the subsequent sections.

Default Built-in Error Handler of Express

When we use the Express framework to build our web applications, we get an error handler by default that catches and processes all the errors thrown in the application.

Let us check this behavior with the help of this simple Express application with a route that throws an error:

const express = require('express')

const app = express()

// handle get request for path /productswitherror
app.get('/productswitherror', (request, response) => {
  
  // throw an error with status code of 400
  let error = new Error(`processing error in request at ${request.url}`)
  error.statusCode = 400
  throw error

})

const port = 3000
app.listen(3000, 
     () => console.log(`Server listening on port ${port}.`));

When we invoke this route with URL /productswitherror, we will get an error with a status code of 400 and an error message: processing error in request .... But we do not have to handle this error since it is handled by the default error handler of the Express framework.

When we call this route either by putting this URL in a browser or by running a CURL command in a terminal window, we will get an error stack contained in an HTML format as output as shown:

Error: processing error in request at /productswitherror
    at /.../storefront/js/index.js:43:15
    at Layer.handle .. (/.../storefront/node_modules/express/lib/router/layer.js:95:5)
    at next (/.../storefront/node_modules/express/lib/router/route.js:137:13)
    at Route.dispatch (/.../storefront/node_modules/express/lib/router/route.js:112:3)
    at Layer.handle .. (/.../storefront/node_modules/express/lib/router/layer.js:95:5)
    at /.../storefront/node_modules/express/lib/router/index.js:281:22
    at Function.process_params (/.../storefront/node_modules/express/lib/router/index.js:341:12)
    at next (/.../storefront/node_modules/express/lib/router/index.js:275:10)
    at SendStream.error (/.../storefront/node_modules/serve-static/index.js:121:7)
    at SendStream.emit (node:events:390:28)

This is the error message sent by the Express framework’s default error handler. Express catches this error for us and responds to the caller with the error’s status code, message, and stack trace (only for non-production environments). But this behavior applies only to synchronous functions.

However, the asynchronous functions that are called from route handlers which throw an error, need to be handled differently. The error from asynchronous functions are not handled by the default error handler in Express and result in the stopping (crashing) of the application.

To prevent this behaviour, we need to pass the error thrown by any asynchronous function invoked by route handlers and middleware, to the next()function as shown below:

const asyncFunction = async (request,response,next) => {
  try {
    throw new Error(`processing error in request `)
  } catch(error) {
    next(error)
  }  
}

Here we are catching the error and passing the error to the next() function. Now the application will be able to run without interruption and invoke the default error handler or any custom error handler if we have defined it.

However, this default error handler is not very elegant and user-friendly giving scant information about the error to the end-user. We will improve this behavior by adding custom error handling functions in the next sections.

Handling Errors with Error Handling Middleware Functions

An Express application is essentially a series of middleware function calls. We define a set of middleware functions and attach them as a stack to one or more route handler functions. We call the next middleware function by calling the next() function.

The error handling middleware functions are defined in the same way as other middleware functions and attached as a separate stack of functions:

Express Error Handling Middleware Functions

When an error occurs, we call the next(error) function and pass the error object as input. The Express framework will process this by skipping all the functions in the middleware function stack and triggering the functions in the error handling middleware function stack.

The error handling middleware functions are defined in the same way as other middleware functions, but they accept the error object as the first input parameter followed by the three input parameters: request, response, and next accepted by the other middleware functions as shown below:

const express = require('express')
const app = express()

const errorHandler = (error, request, response, next) {
  // Error handling middleware functionality
}

// route handlers
app.get(...)
app.post(...)

// attach error handling middleware functions after route handlers
app.use(errorHandler)

These error-handling middleware functions are attached to the app instance after the route handler functions have been defined.

The built-in default error handler of Express described in the previous section is also an error-handling middleware function and is attached at the end of the middleware function stack if we do not define any error-handling middleware function.

Any error in the route handlers gets propagated through the middleware stack and is handled by the last middleware function which can be the default error handler or one or more custom error-handling middleware functions if defined.

Calling the Error Handling Middleware Function

When we get an error in the application, the error object is passed to the error-handling middleware, by calling the next(error) function as shown below:

const express = require('express')
const axios = require("axios")
const app = express()

const errorHandler = (error, request, response, next) {
  // Error handling middleware functionality
  console.log( `error ${error.message}`) // log the error
  const status = error.status || 400
  // send back an easily understandable error message to the caller
  response.status(status).send(error.message)
}

app.get('/products', async (request, response) => {
  try {
    const apiResponse = await axios.get("http://localhost:3001/products")

    const jsonResponse = apiResponse.data
    
    response.send(jsonResponse)
  } catch(error) {
    next(error) // calling next error handling middleware
  }

})
app.use(errorHandler)

As we can see here, the next(error) function takes the error object in the catch block as input which is passed on to the next error-handling middleware function where we can potentially put the logic to extract relevant information from the error object, log the error, and send back an easily understandable error message to the caller.

Adding Multiple Middleware Functions for Error Handling

We can chain multiple error-handling middleware functions similar to what we do for other middleware functions.

Let us define two middleware error handling functions and add them to our routes:

const express = require('express')
const app = express()

// Error handling Middleware function for logging the error message
const errorLogger = (error, request, response, next) => {
  console.log( `error ${error.message}`) 
  next(error) // calling next middleware
}

// Error handling Middleware function reads the error message 
// and sends back a response in JSON format
const errorResponder = (error, request, response, next) => {
  response.header("Content-Type", 'application/json')
    
  const status = error.status || 400
  response.status(status).send(error.message)
}

// Fallback Middleware function for returning 
// 404 error for undefined paths
const invalidPathHandler = (request, response, next) => {
  response.status(404)
  response.send('invalid path')
}

// Route with a handler function which throws an error
app.get('/productswitherror', (request, response) => {
  let error = new Error(`processing error in request at ${request.url}`)
  error.statusCode = 400
  throw error
})

app.get('/products', async (request, response) => {
  try{
    const apiResponse = await axios.get("http://localhost:3001/products")

    const jsonResponse = apiResponse.data
    
    response.send(jsonResponse)
  }catch(error){
    next(error) // calling next error handling middleware
  }

})

// Attach the first Error handling Middleware
// function defined above (which logs the error)
app.use(errorLogger)

// Attach the second Error handling Middleware
// function defined above (which sends back the response)
app.use(errorResponder)

// Attach the fallback Middleware
// function which sends back the response for invalid paths)
app.use(invalidPathHandler)

app.listen(PORT, () => {
  console.log(`Server listening at http://localhost:${PORT}`)
})

These middleware error handling functions perform different tasks:

  • errorLogger logs the error message
  • errorResponder sends the error response to the caller

We have then attached these two error-handling middleware functions to the app object, after the definitions of the route handler functions by calling the use() method on the app object.

To test how our application handles errors with the help of these error handling functions, let us invoke the with URL: localhost:3000/productswitherror. The error raised from this route causes the first two error handlers to be triggered. The first one logs the error message to the console and the second one sends the error message processing error in request at /productswitherror in the response.

We have also added a middleware function invalidPathHandler() at the end of the chain which will be a fallback function to handle requests whose routes are not defined.

Please note that the function invalidPathHandler() is not an error-handling middleware since it does not take an error object as the first parameter. It is a conventional middleware function that gets invoked at the end of the middleware stack.

When we request a non-existent route in the application for example: http://localhost:3000/productswitherrornew, Express does not a find any matching routes. So it does not invoke any route handler functions and associated middleware and error handling functions. It invokes only the middleware function invalidPathHandler() at the end which sends an error message: invalid path with an HTTP status code of 404.

Error Handling while Calling Promise-based Methods

Lastly, it will be worthwhile to look at the best practices for handling errors in JavaScript Promise blocks. A Promise is a JavaScript object which represents the eventual completion (or failure) of an asynchronous operation and its resulting value.

We can enable Express to catch errors in Promises by providing next as the final catch handler as shown in this example:

app.get('/product',  (request, response, next) => {
 
    axios.get("http://localhost:3001/product")
    .then(response=>response.json)
    .then(jsonresponse=>response.send(jsonresponse))
    .catch(next)
})

Here we are calling a REST API with the axios library which returns a Promise and catches any error in the API invocation by providing next() as the final catch handler.

According to the Express Docs, from Express 5 onwards, the route handlers and middleware functions that return a Promise will call next(value) automatically when they reject or throw an error.

Developing Express Error Handling Middleware with TypeScript

TypeScript is an open-source language developed by Microsoft. It is a superset of JavaScript with additional capabilities, most notable being static type definitions making it an excellent tool for a better and safer development experience.

Let us first add support for TypeScript to our Node.js project and then see a snippet of the error handling middleware functions written using the TypeScript language.

Installing TypeScript and other Configurations

For adding TypeScript, we need to perform the following steps:

  1. Install Typescript and ts-node with npm:
npm i -D typescript ts-node
  1. Create a JSON file named tsconfig.json with the below contents in our project’s root folder to specify different options for compiling the TypeScript code as shown here:
{
  "compilerOptions": {
    "module": "commonjs",
    "target": "es6",
    "rootDir": "./",
    "esModuleInterop": true
  }
}
  1. Install the type definitions of the Node APIs and Express to be fetched from the @types namespace by installing the @types/node and @types/express packages as a development dependency:
npm i -D @types/node @types/express

Writing the Express Error Handling Middleware Functions in TypeScript

After enabling the project for TypeScript, we have written the same application built earlier in TypeScript. The files for TypeScript are kept under the folder: ts. Here is a snippet of the code in file app.ts containing routes and error handling middleware functions:

  import express, { Request, Response, NextFunction } from 'express'

  const app = express()
  const port: number = 3000


  // Error object used in error handling middleware function
  class AppError extends Error{
      statusCode: number;

      constructor(statusCode: number, message: string) {
        super(message);
    
        Object.setPrototypeOf(this, new.target.prototype);
        this.name = Error.name;
        this.statusCode = statusCode;
        Error.captureStackTrace(this);
      }
  }
  
  // Middleware function for logging the request method and request URL
   const requestLogger = (
    request: Request, 
    response: Response, 
    next: NextFunction) => {

      console.log(`${request.method} url:: ${request.url}`);
      next()
  }

  app.use(requestLogger)  

  app.use('/products', express.json({ limit: 100 }))

  // Error handling Middleware functions

  // Error handling Middleware function for logging the error message
  const errorLogger = (
        error: Error, 
        request: Request, 
        response: Response, 
        next: NextFunction) => {
          console.log( `error ${error.message}`) 
          next(error) // calling next middleware
    }
    
  // Error handling Middleware function reads the error message 
  // and sends back a response in JSON format  
  const errorResponder = (
      error: AppError, 
      request: Request, 
      response: Response, 
      next: NextFunction) => {
          response.header("Content-Type", 'application/json')
            
          const status = error.statusCode || 400
          response.status(status).send(error.message)
    }

  // Fallback Middleware function for returning 
  // 404 error for undefined paths
  const invalidPathHandler = (
    request: Request, 
    response: Response, 
    next: NextFunction) => {
      response.status(404)
      response.send('invalid path')
  }
  
  
  
  app.get('product', (request: Request, response: Response) => {
      response.sendFile("productsample.html")
  })
  
  // handle get request for path /
  app.get('/', (request: Request, response: Response) => {
      response.send('response for GET request');
  })
  
  
  const requireJsonContent = (
    request: Request, 
    response: Response, 
    next: NextFunction) => {
    if (request.headers['content-type'] !== 'application/json') {
        response.status(400).send('Server requires application/json')
    } else {
      next()
    }
  }


  app.get('/products', async (
    request: Request, 
    response: Response, 
    next: NextFunction) => {
    try{
      const apiResponse = await axios.get("http://localhost:3001/products")

      const jsonResponse = apiResponse.data
      console.log("response " + jsonResponse)
      
      response.send(jsonResponse)
    }catch(error){
      next(error)
    }

  })

  app.get('/product',  (
    request: Request, 
    response: Response, 
    next: NextFunction) => {
   
      axios.get("http://localhost:3001/product")
      .then(jsonresponse=>response.send(jsonresponse))
      .catch(next)
  })

  app.get('/productswitherror', (
    request: Request, 
    response: Response) => {

    let error:AppError = new AppError(400, 
      `processing error in request at ${request.url}`)

    error.statusCode = 400
    throw error
  })

    
  // Attach the first Error handling Middleware
  // function defined above (which logs the error)  
  app.use(errorLogger)

  // Attach the second Error handling Middleware
  // function defined above (which sends back the response)
  app.use(errorResponder)

  // Attach the fallback Middleware
  // function which sends back the response for invalid paths)
  app.use(invalidPathHandler)


  app.listen(port, () => {
      console.log(`Server listening at port ${port}.`)
  }
)

Here we have used the express module to create an application as we have seen before. With this configuration, the application will run on port 3000 and can be accessed with the URL: http://localhost:3000.

We have modified the import statement on the first line to import the TypeScript interfaces that will be used for the request, response, and next parameters inside the Express middleware.

Running the Express Application Written in TypeScript

We run the Express application written in TypeScript code by using the below command:

Running this command will start the HTTP server. We have used npx here which is a command-line tool that can execute a package from the npm registry without installing that package.

Conclusion

Here is a list of the major points for a quick reference:

  1. We perform error handling in Express applications by writing middleware functions that handle errors. These error handling functions take the error object as the fourth parameter in addition to the parameters: request, response, and the next() function.

  2. Express comes with a default error handler for handling error conditions. This is a default middleware function added by Express at the end of the middleware stack.

  3. We call the error handling middleware by passing the error object to the next(error) function.

  4. We can define a chain of multiple error-handling middleware functions to one or more routes and attach them at the end of Express route definitions.

  5. We can enable Express to catch errors in JavaScript Promises by providing next as the final catch handler.

  6. We also used TypeScript to author an Express application with route handler and error-handling middleware functions.

You can refer to all the source code used in the article on Github.

 on
April 29, 2021

A Guide to Error Handling in Express.js

Error handling often doesn’t get the attention and prioritization it deserves. Especially for newbie developers, there is more focus on setting up routing, route handlers, business logic, optimizing performance, etc. As a result, the equally (if not more) crucial error-handling part will likely be overlooked. Striving for the most optimized code and squeezing out every last ounce of performance is all well and good; yet, it’s important to remember all it takes is one unhandled error leak into your user interface to override all the seconds you helped your users save.

Because there are so many components involved in a successful, functioning web application, it is vital to foolproof your application by preparing for all possible errors and exceptions. If left mishandled, these errors can lead to a bad user experience and end up affecting your business. At the same time, errors provide critical information about potential errors in your application that could bring the whole thing down. Therefore, you must be thoughtful and intelligent about error handling in your application. 

This post will c, Node.js’s most popular server-side framework (even though most of these concepts apply to other frameworks too). Express does a great job taking care of several unhandled errors and provides an easy-to-use, flexible API that developers can utilize to build error handling middleware. 

Here’s an outline of what we’ll be covering so you can easily navigate or skip ahead in the guide:

  • How does Error Handling Work in Express.js?
    • Express Middleware Functions
    • Default Error Handling in Express.js
  • Handling Custom Errors
    • Custom Handling for Each Route
    • Writing your own Error Handling Middleware Functions
    • Adding Multiple Middleware Handlers
  • Basic Quick Tutorial: Setting up Error Handling in Express.js

How Does Error Handling Work in Express.js? 

Express.js is the most popular Javascript server-side framework, perhaps, primarily because of its ease of usage and getting started. One of the many ways it makes things easier is by automatically catching all errors in route handlers, and allowing developers to extend route handling functionalities by leveraging useful middleware functions. 

Before we see how all of this works, let’s briefly visit the concept of middleware functions in Express – most error handling functionality is achieved through these functions. 

Express Middleware Functions 

Middleware functions in Express are essentially functions that come into play after the server receives the request and before the response fires to the client. They have access to the request and the response objects. They can be used for any data processing, database querying, making API calls, sending the response, or calling the next middleware function (using the next() function). 

Two aspects of middleware functions to keep in mind are:

  • They are triggered sequentially (top to bottom) based on their sequence in code.
  • They operate until the process exits, or the response has been sent back to the client.

Let’s understand this through a small example. Below we define two middleware functions using the .use() function and one route handler (skipping the boilerplate code for the sake of simplicity):

app.use((req, res, next) => {
  console.log("Middleware 1 called.")
  console.log(req.path)
  next() // calling next middleware function or handler
})

app.get('/', (req, res) => {
  console.log("Route handler called.")
  res.send("Hello world!") // response sent back – no more middleware called
})

app.use((req, res, next) => {
  console.log("Last middleware called❓") // not called
})

Here, each time the server receives a request, the first middleware is fired, followed by the corresponding route handler (using the next() function). However, because the response returns in this handler, the last middleware function is not called. Here’s the output:

undefined
Server output

Several native as well as third-party middleware functions have been made available by the Express community and are widely for adding functionalities like session management, authentication, logging, redirecting, and so much more. This was a basic example of how middleware functions work. We will come back to them when discussing how to utilize them for error handling in our applications.

Default Error Handling in Express.js 

Express implicitly takes care of catching your errors to prevent your application from crashing when it comes to error handling. This is especially true for synchronous route handler code. Let’s see how:

Synchronous Code

Synchronous code refers to statements of code that execute sequentially and one at a time. When an error encounters synchronous code, Express catches it automatically. Here’s an example of a route handler function where we simulate an error condition by throwing an error:

app.get('/', (req, res) => {
  throw new Error("Hello error!")
})

Express catches this error for us and responds to the client with the error’s status code, message, and even the stack trace (for non-production environments).

All of this is taken care of thanks to Express’s default built-in error handler middleware function inserted at the end of your code’s middleware stack. This automatic handling saves you from bulky try/catch blocks and explicit calls to the in-built middleware (shown below) while also providing some fundamental default error handling functionality. 

app.get('/', (req, res, next) => {
  try {
      throw new Error("Hello error!")
  }
  catch (error) {
      next(error)
  }
})

You can also choose to create your own middleware function to specify your error handling logic. 

Asynchronous Code

When writing server-side code, most of your route handlers are likely using asynchronous Javascript logic to read and write files on the server, query databases, and make external API requests. Let’s see whether Express can catch errors raised from asynchronous code as well. We’ll throw an error from inside the asynchronous setTimeout() function and see what happens:

app.get('/', (req, res) => {
  setTimeout(() => {
      console.log("Async code example.")
      throw new Error("Hello Error!")
  }, 1000)
})

As you can see, our server crashed because Express didn’t handle the error for us. 

undefined
Server output

For handling errors raised during asynchronous code execution in Express (versions < 5.x), developers need to themselves catch their errors and invoke the in-built error handler middleware using the next() function. Here’s how:

app.get('/', (req, res, next) => {
  setTimeout(() => {
      try {
          console.log("Async code example.")
          throw new Error("Hello Error!")
      } catch (error) { // manually catching
          next(error) // passing to default middleware error handler
      }
  }, 1000)
})
undefined
Browser output

This is much better – we caught the error, and our server didn’t crash. This does look a little bulky because we used the setTimeout() function to demonstrate async behavior. This function does not return a promise and, therefore, can’t be chained with a quick .catch() function. However, most libraries that help with async operations return promises these days (e.g., the file system API). Below is an example of a more convenient and common way of catching errors from promises:

const fsPromises = require('fs').promises
app.get('/', (req, res, next) => {
  fsPromises.readFile('./no-such-file.txt')

     .then(data => res.send(data))

     .catch(err => next(err)) 
})

Note: Express 5.0 (currently in alpha) can automatically catch errors (and rejections) thrown by returned Promises. 

Handling Custom Errors 

Express’s default error-handling middleware is super helpful for beginners to take care of unexpected, unhandled errors. However, different developers and organizations would want their errors handled in their own way – some might want to write these to log files, others might want to alert the user or redirect them to another page, or all of the above.

Custom Handling for Each Route 

An obvious, naive way of going about this would be to define your custom error handling logic for each route handler as so:

const express = require('express')
const fsPromises = require('fs').promises;

const app = express()
const port = 3000

app.get('/one', (req, res) => {
  fsPromises.readFile('./one.txt')
    .then(data => res.send(data))
    .catch(err => { // error handling logic 1
        console.error(err) // logging error
        res.status(500).send(err)
    })
})

app.get('/two', (req, res) => {
  fsPromises.readFile('./two.txt')
    .then(data => res.send(data))
    .catch(err => { // error handling logic 2
        console.error(err)
        res.redirect('/error') // redirecting user
    })
})

app.get('/error', (req, res) => {
  res.send("Custom error landing page.")
})

app.listen(port, () => {
  console.log(`Example app listening at http://localhost:${port}`)
})

Here, we specified two different handling logics – one for each route that attempts to read arbitrary files on the server. As you can imagine, this would get too redundant quickly and wouldn’t scale well as you add more and more routes.

Writing your Error Handling Middleware Functions 

A much better option would be to leverage Express’s middleware functions here. You could write one or more middleware functions for handling errors in your application that all of your routes could utilize by making simple next() calls. 

Middleware functions are much more convenient to work with than conventional functions because they automatically have access to the error, request, and response objects and can be invoked (or invoke others) based on their ordering using just the next() function.

You can create your own error handling middleware functions by adding the error argument to the function, apart from request, response, and next. Here is an example:

app.use((error, req, res, next) => {
  console.log("Error Handling Middleware called")
  console.log('Path: ', req.path)
  next() // (optional) invoking next middleware
})

Another thing to keep in mind is the ordering of the middleware. The error handler needs to specify middleware functions after the route handlers for the next(error) calls to be directed towards them.

Now let’s recreate the previous example, but this time with an error-handling middleware in place.

const express = require('express')
const fsPromises = require('fs').promises

const app = express()
const port = 3000

app.get('/one', (req, res, next) => {
  fsPromises.readFile('./one.txt') // arbitrary file
    .then(data => res.send(data))
    .catch(err => next(err)) // passing error to custom middleware
})

app.get('/two', (req, res, next) => {
  fsPromises.readFile('./two.txt')
    .then(data => res.send(data))
    .catch(err => {
        err.type = 'redirect' // custom prop to specify handling behaviour
        next(err)
    })
})

app.get('/error', (req, res) => {
  res.send("Custom error landing page.")
})

app.use((error, req, res, next) => {
  console.log("Error Handling Middleware called")
  console.log('Path: ', req.path)
  console.error('Error: ', error)
 
  if (error.type == 'redirect')
      res.redirect('/error')

   else if (error.type == 'time-out') // arbitrary condition check
      res.status(408).send(error)
  else
      res.status(500).send(error)
})


app.listen(port, () => {
  console.log(`Example app listening at http://localhost:${port}`)
})

Instead of defining the handling behavior inside each route, we place all our logic inside the middleware. Then, based on the kind of error, we can modify the error object (or throw a custom error) and accordingly deal with it in the middleware.

This allows us to achieve the same functionality as before, but more efficiently. Assuming these files are not present on the server, if we go to /one, the server logs the error and sends back a 500 (internal server error) response. We are redirected to the /error page after the error logs if we open /two. Below are the corresponding client and server outputs:

As you can imagine, this was a fairly basic example just to give you a sense of how you can decouple your error handling logic from the route handling into a middleware function. This extends to larger applications with hundreds of routes for increased modularity, reduced redundancy, easier maintenance, and more efficient exception handling.

Adding Multiple Middleware Handlers 

In the previous section, we worked with just one middleware to handle all our errors. However, in practice, multiple middleware functions are usually employed for different aspects of error handling to have further abstractions. For example, one middleware for logging errors, another for responding to the client, perhaps another as a fail-safe catch-all handler, etc. Here’s a preview of the same based on our previous example:

// route handlers
app.get('/one')
app.get('/two') 

app.get('/error')
// middleware

app.use(errorLogger)
app.use(errorResponder)
app.use(failSafeHandler)

Let’s write the code for this.

const express = require('express')
const fsPromises = require('fs').promises

const app = express()
const port = 3000

app.get('/one', (req, res, next) => {
  fsPromises.readFile('./one.txt')
  .then(data => res.send(data))
  .catch(err => next(err)) // passing error to custom middleware
})

app.get('/two', (req, res, next) => {
  fsPromises.readFile('./two.txt')
  .then(data => res.send(data))
  .catch(err => {
      err.type = 'redirect' // adding custom property to specify handling behaviour
      next(err)
  })
})

app.get('/error', (req, res) => {
  res.send("Custom error landing page.")
})

function errorLogger(error, req, res, next) { // for logging errors
  console.error(error) // or using any fancy logging library
  next(error) // forward to next middleware
}

function errorResponder(error, req, res, next) { // responding to client
  if (error.type == 'redirect')
      res.redirect('/error')
  else if (error.type == 'time-out') // arbitrary condition check
      res.status(408).send(error)
  else
      next(error) // forwarding exceptional case to fail-safe middleware
}

function failSafeHandler(error, req, res, next) { // generic handler
  res.status(500).send(error)
}

app.use(errorLogger)
app.use(errorResponder)
app.use(failSafeHandler)

app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`)
})

This allows us to achieve the same functionality as in the previous code example, but in a more modular way that would scale better as you add more routes and handle more error conditions.

However, as previously discussed, when working with multiple middleware functions, one must keep an eye on their sequence and remember that each middleware should either respond to the client or invoke the subsequent one in the stack. If the server is just left to hang, the client continues to wait. For example, if we missed using next() in the first middleware (errorLogger), the subsequent middleware functions are not invoked, and therefore, no response fires. 

Basic Quick Tutorial: Setting up Error Handling in Express.js

Now that we’ve covered almost all aspects of error handling in Express, theory-wise, let’s solidify our understanding of these concepts by creating a prototype Express application that handles errors using middleware methods in a relatively more realistic setting.

We’ll create an API that serves user posts data fetched from a dummy API (jsonplaceholder.typicode.com). We will then validate some of the posts’ properties based on some arbitrary criteria (e.g., the content length), raise custom errors if validation fails, capture these using our custom middleware, and process them accordingly. 

Step 1: Create and Setup Project 

First, create an empty folder, cd into it, generate an npm project, and install the dependencies.

mkdir my-express-app && cd my-express-app
npm init -y
npm i --save express node-fetch

Then, create files – index.js, routes.js, errors.js, and middleware.js. It is considered good practice to keep your routes, main file, and other utilities in separate files. Ideally, developers prefer different folders for better organization, but for our small prototype, just files would suffice.

undefined
Project files

Step 2: Setup the Server 

Now let’s write the code that will start our server in index.js.

// index.js
const express = require('express')
const app = express()
const port = 3000

app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`)
})

We’ll start the server and make sure everything is working fine by running the node index.js command from inside the folder.

undefined
Console output

Step 3: Create Some Routes 

Now let’s create some routes in the routes.js file, and for now, just fetch some dummy JSON posts data from the dummy API (jsonplaceholder.typicode.com/posts), and serve it through our route. We will use Express’s Router module and export our routes – to import into our main index.js server file.

// routes.js
const express = require('express')
const fetch = require('node-fetch') // for making external API requests
const router = express.Router()
router.get('/', (req, res) => {
    res.send("Hello World!")
})

router.get('/user-posts', (req, res, next) => {
  fetch('https://jsonplaceholder.typicode.com/posts')
      .then(res => res.json())
      .then(data => {
          console.log(data)
          res.header("Content-Type",'application/json');
          res.send(JSON.stringify(data, null, 4)) // pretty print
        })
      .catch(err => next(err)) // pass to default error handler middleware
})

router.get('/error', (req, res) => {
  res.send("The URL you are trying to reach does not exist.")
})

module.exports = router // export routes

Now let’s import these routes into our server file.

// index.js
const express = require('express')
const routes = require('./routes') // importing routes

const app = express()
const port = 3000

app.use(routes) // initializing routes

app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`)
})

Let’s fire up our browser and check whether the route is working.

undefined
Browser output

Our server seems to be working fine here. Let’s do some error handling now.

Step 4: Creating and Handling Custom Errors 

It is pretty helpful to create custom error classes for your applications by extending Node’s Error class in practice. These errors can raise issues specific to the application, for example – unauthorized access, unsuccessful payment, incorrect user input, etc. This allows developers to have more detailed information about the error conditions (through custom error messages and other properties), and therefore handle them better.

In our use case, let’s say we want to ensure that all the posts have a title of fewer than 100 characters and a body character count of fewer than 220 characters. If we don’t meet this condition, we want to raise a custom error message that alerts the developer about the same time. 

Now that we have the error condition in mind, let’s create our custom error classes in the errors.js file.

// errors.js
class CharacterCountExceeded extends Error { // parent error
  constructor(post_id, content) {
      super();
      this.name = this.constructor.name // good practice

      if (this instanceof LongTitleError) // checking if title or body
          this.type = 'title'
      else if (this instanceof LongBodyError)
          this.type = 'body'
 
    this.message = `The character count of post (id: ${post_id}) ${this.type} is too long. (${content.length} characters)` // detailed error message
    this.statusCode = 500 // error code for responding to client
  }
}

// extending to child error classes
class LongTitleError extends CharacterCountExceeded { }
class LongBodyError extends CharacterCountExceeded { }

module.exports = {
    CharacterCountExceeded,
    LongTitleError,
    LongBodyError
}

First, we create one parent error class (CharacterCountExceeded) for all errors that involve an exceeded character count. The constructor for this class accepts the post’s ID and the content (of the title or body) to generate the required error message and specify an error code. Then we extend this class to create two more specific children classes (LongTitleError and LongBodyError) that refer to the particular error condition.

Now we will import these into our routes.js file, check for erroneous conditions inside our route handler, and throw these custom errors wherever required.

// routes.js
const express = require('express')
const fetch = require('node-fetch')
const router = express.Router()
const { LongTitleError, LongBodyError } = require('./errors');
router.get('/', (req, res) => {
  res.send("Hello World!")
})

router.get('/user-posts', (req, res, next) => {
  fetch('https://jsonplaceholder.typicode.com/posts')
      .then(res => res.json())
      .then(posts => {
          for (post of posts) {
              if (post.title.length > 100)
                  throw new LongTitleError(post.id, post.body)
              if (post.body.length > 220)
                  throw new LongBodyError(post.id, post.body) 
          }
          console.log(posts)
          res.header("Content-Type", 'application/json')
          res.send(JSON.stringify(posts, null, 4)) // pretty print
      })
      .catch(err => next(err))
})

router.get('/error', (req, res) => {
    res.send("The URL you are trying to reach does not exist.")
})

module.exports = router

As you can see here, we traverse through all the posts, check for their title and body’s character count, and throw our custom errors accordingly. Here’s the output:

undefined

It turns out there was one post that had a body size of more than 220 characters, and we successfully captured it. At the moment, we are forwarding all our errors through the catch block to Express’s default error handler middleware. But what’s the fun in that?

Let’s create our own middleware functions and use them as we like.

Step 5: Adding Custom Error Handler Middleware

We’ll use the middleware.js file that we created before.

// middleware.js
const errorLogger = (err, req, res, next) => {
  console.error('x1b[31m', err) // adding some color to our logs
  next(err) // calling next middleware
}

const errorResponder = (err, req, res, next) => {
  res.header("Content-Type", 'application/json')
  res.status(err.statusCode).send(JSON.stringify(err, null, 4)) // pretty print
}
const invalidPathHandler = (req, res, next) => {
  res.redirect('/error')
}

module.exports = { errorLogger, errorResponder, invalidPathHandler }

Here, we add three middleware functions – one for logging errors, one for sending the error to the client, and one for redirecting a user from an invalid route to an error landing page. Now let’s import these into our main file and use them in our application.

// index.js
const express = require('express')
const routes = require('./routes')
const { errorLogger, errorResponder, invalidPathHandler } = require('./middleware')

const app = express()
const port = 3000

app.use(routes)

// middleware
app.use(errorLogger)
app.use(errorResponder)
app.use(invalidPathHandler)

app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`)
})

Now let’s open our browser and see whether the middleware handles our LongBody error the way it’s supposed to.

undefined
Console output: Error object logged in red using the errorLogger middleware.
undefined
Server’s error response using the errorResponder middleware.
undefined
Redirecting to the error landing page upon encountering an invalid path

As you can see all our middleware functions are working as expected – in logging, responding to the client, and redirecting to the error landing page.

Wrapping it Up

We covered everything about error handling in Express.js – from default error handling of synchronous and asynchronous code to creating your own error classes and writing your own error-handling middleware functions. 

Now go ahead and make sure to handle all your errors in your Express application in a clean, non-redundant, efficient, and easy to maintain way. And if you haven’t already, write your own middleware functions and play around with native and third-party ones to explore how they can be helpful for your applications. If you are serious about your application’s performance and want to spend less time debugging issues and more time building new features, consider checking out ScoutAPM for monitoring your Node.js app’s performance and get started with a 14-day free trial.

Alt Text

Github repository

Why do we need error handling at all?

Imagine that a client application (web, mobile…) is using the server. Sometimes we need to handle exceptions which appear in the application and we need to send a clear message to the user what is wrong. It is very important to have a working error handler inside the application in order to achieve better user experience and for many other reasons as well. Beside from the user experience, it is a good practice to catch errors in one place (all the errors go through the handler) so the developer can track the bugs/exceptions more easily.

Creating exceptions

An exception is created using the throw keyword inside the application.

throw Error('Error');

Enter fullscreen mode

Exit fullscreen mode

As soon as the application executes this line the normal flow is halted and the control is switched to the nearest exception handler. While in some other environments we can throw strings, objects etc, in Node.js we throw error objects. An error object is an object derived from Error or an instance of Error itself.

Throwing an error from derived Error object looks like:

class SomethingIsWrongError extends Error {
  constructor() {
    super('Something is wrong!');
  }
}
throw new SomethingIsWrongError();

Enter fullscreen mode

Exit fullscreen mode

Before we start creating our error handler we need to decide what is the right way to go. Most of my applications have supported/support multiple languages which means that the message needs to be translated into the language which the user has selected. We cannot show errors in English language if the user has selected Japanese language which means the error message needs to be translated somewhere. Either we translate the message on the server side or on the client side.

  • Server side translation
    In order to translate the message on the server side we have to know to whom we are sending the exception in order to get the selected language from the user. Challenge of this approach is that a developer needs always to have the selected language of the user whenever an error message needs to be sent to the client side.

  • Client side translation
    Other solution is to send an unique error code and any additional data if needed so the translation of exceptions should be done on the client side based on the code and this is the solution which I prefer.

The client side needs to know:

  • Status code.
  • Unique error code. Every error has its own unique code.
  • Metadata if any. If any additional dynamic data needs to be sent in order to translate the message like what is the maximum allowed input number etc.

In order to keep track of all the errors more easily, we need to create a class in which we will store all possible errors that we know about. When we throw an exception then we will refer to one of the codes found in that class.

Create a folder called error-handler in the root directory and this will be the place where we will create files for error handler logic. Create a file called error-code.ts with following code:

export class ErrorCode {
  public static readonly Unauthenticated = 'Unauthenticated';
  public static readonly NotFound = 'NotFound';
  public static readonly MaximumAllowedGrade = 'MaximumAllowedGrade';
  public static readonly AsyncError = 'AsyncError';
  public static readonly UnknownError = 'UnknownError';
}

Enter fullscreen mode

Exit fullscreen mode

We also need to have a model that we will return to the client. Create a file called error-model.ts inside error-handler folder with following code:

export class ErrorModel {
  /**
   * Unique error code which identifies the error.
   */
  public code: string;
  /**
   * Status code of the error.
   */
  public status: number;
  /**
   * Any additional data that is required for translation.
   */
  public metaData?: any;
}

Enter fullscreen mode

Exit fullscreen mode

And now we need to create the actual error exception object. Create a file called error-exception.ts inside error-handler folder with following code:

import { ErrorCode } from './error-code';

export class ErrorException extends Error {
  public status: number = null;
  public metaData: any = null;
  constructor(code: string = ErrorCode.UnknownError, metaData: any = null) {
    super(code);
    Object.setPrototypeOf(this, new.target.prototype);
    this.name = code;
    this.status = 500;
    this.metaData = metaData;
    switch (code) {
      case ErrorCode.Unauthenticated:
        this.status = 401;
        break;
      case ErrorCode.MaximumAllowedGrade:
        this.status = 400;
        break;
      case ErrorCode.AsyncError:
        this.status = 400;
        break;
      case ErrorCode.NotFound:
        this.status = 404;
        break;
      default:
        this.status = 500;
        break;
    }
  }
}

Enter fullscreen mode

Exit fullscreen mode

When we want to throw an error from our application we use exactly the class we created and one code from the available list of codes. We would throw an error like:

throw new ErrorException(ErrorCode.MaximumAllowedGrade, { max: 100 }); // object is optional

Enter fullscreen mode

Exit fullscreen mode

Error handler

Error handler is a special middleware in Node.js which takes 4 parameters. Regular route middleware takes 3 parameters: req, res and next. Error handler also takes these 3 parameters and one additional parameter which is the actual error. Those four parameters are (retrospectively):

  1. err
  2. req
  3. res
  4. next

Create file called error-handler.ts inside error-handler folder. The following handler will intercept all errors that occur in the application whether it is an exception that we know or an exception that we do not know. In order to recognize that it is an exception thrown by ourselves, we can recognize it by type of instance if (err instanceof ErrorException)

import { Request, Response, NextFunction } from 'express';
import { ErrorCode } from './error-code';
import { ErrorException } from './error-exception';
import { ErrorModel } from './error-model';

export const errorHandler = (err: Error, req: Request, res: Response, next: NextFunction) => {
  console.log('Error handling middleware called.');
  console.log('Path:', req.path);
  console.error('Error occured:', err);
  if (err instanceof ErrorException) {
    console.log('Error is known.');
    res.status(err.status).send(err);
  } else {
    // For unhandled errors.
    res.status(500).send({ code: ErrorCode.UnknownError, status: 500 } as ErrorModel);
  }
};

Enter fullscreen mode

Exit fullscreen mode

Now it is necessary to register this handler and we will register it as follows. The handler needs to be ‘lowered’ as far as possible in the application after all routes and other middlewares and handlers. If we specify routes or middlewares after registration of errorHandler then the error handler will not catch exceptions which appear in those routes or middlewares.

app.use(errorHandler); // registration of handler

app.listen(3000, () => {
  console.log('Application started on port 3000!');
});

Enter fullscreen mode

Exit fullscreen mode

Now we are ready to throw some errors.

app.get('/throw-unauthenticated', (req: Request, res: Response, next: NextFunction) => {
  throw new ErrorException(ErrorCode.Unauthenticated);
  // or
  // next(new ErrorException(ErrorCode.Unauthenticated))
});
app.get('/throw-maximum-allowed-grade', (req: Request, res: Response, next: NextFunction) => {
  throw new ErrorException(ErrorCode.MaximumAllowedGrade, { grade: Math.random() });
  // or
  // next(new ErrorException(ErrorCode.MaximumAllowedGrade, { grade: Math.random() }))
});
app.get('/throw-unknown-error', (req: Request, res: Response, next: NextFunction) => {
  const num: any = null;
  // Node.js will throw an error because there is no length property inside num variable
  console.log(num.length);
});

Enter fullscreen mode

Exit fullscreen mode

If you look at the code above, you will see that we have 2 known exceptions and one unknown. When we want to throw an exception from a route we can do it with the throw keyword or by calling the next function with an actual exception. Error handler will catch both exceptions. However, when it comes to async logic then it will be solved in another way which we will cover next.

Exceptions with promises

By Exress documentation:
Handling sync code:
Errors that occur in synchronous code inside route handlers and middleware require no extra work. If synchronous code throws an error, then Express will catch and process it. For example:

app.get('/', function (req, res) {
  throw new Error('BROKEN'); // Express will catch this on its own.
});

Enter fullscreen mode

Exit fullscreen mode

Handling async code:
For errors returned from asynchronous functions invoked by route handlers and middleware, you must pass them to the next() function, where Express will catch and process them. For example:

app.get('/', function (req, res, next) {
  fs.readFile('/file-does-not-exist', function (err, data) {
    if (err) {
      next(err); // Pass errors to Express.
    } else {
      res.send(data);
    }
  });
});

Enter fullscreen mode

Exit fullscreen mode

Starting with Express 5, route handlers and middleware that return a Promise will call next(value) automatically when they reject or throw an error. For example:

app.get('/user/:id', async function (req, res, next) {
  // if error appears in getUserById, express will automatically throw an error
  const user = await getUserById(req.params.id);
  res.send(user);
});

Enter fullscreen mode

Exit fullscreen mode

Let’s add code into our application for async code. The code will always throw an error and regarding if we are using express 4 or express 5, the application will catch the error.

const someOtherFunction = () => {
  const myPromise = new Promise((resolve, reject) => {
    setTimeout(() => {
      reject(new ErrorException(ErrorCode.AsyncError));
    }, 1000);
  });
  return myPromise;
};
app.get('/throw-async-await-error', async (req: Request, res: Response, next: NextFunction) => {
  // express 4
  try {
    await someOtherFunction();
  } catch (err) {
    next(err);
    // next line will not work as expected
    // throw err
  }
  // express 5
  // await someOtherFunction();
});

Enter fullscreen mode

Exit fullscreen mode

Wrapping up

In this tutorial we covered what exceptions are and how to throw an exception in application. We learned what we need to consider when handling exceptions in multi language applications. We learned to do everything necessary for the Node.JS application to successfully manage exceptions from creating necessary classes to creating a handler and registering it. And finally we learned how to throw exceptions and what to take care of when throwing exceptions in async or sync blocks.

Comming up: Authentication with JWT.

Improve Article

Save Article

  • Read
  • Discuss
  • Improve Article

    Save Article

    Error handling in Express is referred to as something that handles or processes errors that may come while executing any synchronous code or asynchronous code.

    What do we mean by synchronous or asynchronous code?
    A lot of times, it happens that an operation begins executing but for some reason faces some delay before completion. The common examples of such operations are HTTP requests such as AJAX requests, functions such as setTimeout etc. These operations begin, and then end when the response returns or when the timer ends. While the computer waits for these operations to complete, it moves on busying itself with the next lines of code. It keeps busy, but this causes a significant challenge — any code dependent on previous asynchronous code might be run before that asynchronous code is complete, meaning errors. Have a look below-

    var data = makeAsyncRequest();

    console.log("Data is " + data);

     
    We can say that, when we execute something synchronously, we wait for it to finish before moving on another task. When we execute something asynchronously, we can move on to another task before it finishes. The above problem is solved using callbacks, middleware modules or by using the modern promise and await.

    Catching Errors in Express

    • If synchronous code having route handlers and middleware throws any error, then without any effort and extra work, Express solves it by catching and processing the error without requiring any of our consent. Have a look at the below code –

      app.get('/', function (req, res) {

          throw new Error('Died')

       })

    • If a route handler and middleware invokes asynchronous function which in turn produces some errors, then we have to explicitly pass the error to the next() function, where Express will catch and process them. The following illustration will help you to understand

      app.get('/', function (req, res, next) {

        fs.readFile('/file-is-not-available'

              function (err, data) {

          if (err) {

            next(err) 

          } else {

            res.send(data)

          }

        })

      })

    • Route handlers and middlewares that return a Promise will call next(value) automatically when they reject or throw an error.

      app.get('/user/:id', async function (req, res, next) { 

          var user = await getUserById(req.params.id)    

          res.send(user)

      })

      The await keyword can be used to indicate that the function that follows will be returning a promise, which should be awaited before executing any other dependent code. ‘await’ can only be used inside an async function.
      Next (next) will be called with either the thrown error or the rejected value in case if getUserById throws an error or rejects. If no rejected value is provided, next will be called with a default Error object provided by the Express router. If we pass anything to the next() function (except the string ‘route’), Express regards the current request as being an error and will skip any remaining non-error handling routing and middleware functions.

    • If a given callback in a sequence provides no data and only errors, then the code can be simplified as –

      app.get('/', [

        function (req, res, next) {

          fs.writeFile('/path-cannot-be-accessed',

                  'data', next)

        },

        function (req, res) {

          res.send('OK')

        }

      ])

      In the above code, next is provided as the callback which runs without caring whether errors comes out or not. If there is no error, then the second handler also runs otherwise express just catches and processes the error.

      Now, look at the example below – 

      app.get('/', function (req, res, next) {

        setTimeout(function () {

          try {

            throw new Error('Died')

          } catch (err) {

            next(err)

          }

        }, 100)

      })

    • As we know, if a route handler and middleware invokes an asynchronous function which in turn produces some errors, then we have to explicitly pass the error to the next() function, where Express will catch and process them. However, in the above code, the error is not the part of the synchronous code, so we can’t simply pass it to the next function. We need to first throw the errors, catch those errors generated by asynchronous code, and then pass it to the Express. For this, we need to use the try..catch block to catch them. If you don’t want to use try and catch, then simply use promises as shown below –

      app.get('/', function (req, res, next) {

        Promise.resolve().then(function () {

          throw new Error('Died')

        }).catch(next)

      })

      Since promises automatically catch both synchronous errors and rejected promises, you can simply provide next as the final catch handler and Express will catch errors, because the catch handler is given the error as the first argument.

    • Default Error Handlers: The default error handler catches the error when we call next and don’t handle it with a custom error handler. If we want to send a different response to the default, we have to write our own error handler. This default error-handling middleware function is added at the end of the middleware function stack. If you pass an error to next() and you do not handle it in a custom error handler, it will be handled by the built-in error handler, the error will be written to the client with the stack trace. The stack trace is not included in the production environment.

      When an error is written, the following information is added automatically to the response:

      • The res.statusCode is set from err.status (or err.statusCode). If this value is outside the 4xx or 5xx range, it will be set to 500.
      • The res.statusMessage is set as per the status code.
      • The body will be the HTML of the status code message when in production environment, otherwise will be err.stack.
      • Any headers specified in an err.headers object.

      If you call next() with an error after you have started writing the response (for example, if you encounter an error while streaming the response to the client) the Express default error handler closes the connection and fails the request.

      So when you add a custom error handler, you must delegate to the default Express error handler, when the headers have already been sent to the client:

      function errorHandler (err, req, res, next) {

        if (res.headersSent) {

          return next(err)

        }

        res.status(500)

        res.render('error', { error: err })

      }

      Note that the default error handler can get triggered if you call next() with an error in your code more than once, even if custom error handling middleware is in place.

      How to write Error handlers?

      The way we declare the middleware functions, in the same way, error handling functions are defined. However, error-handling functions have four arguments instead of three: (err, req, res, next). For example – 

      app.use(function (err, req, res, next) {

        console.error(err.stack)

        res.status(500).send('Something broke!')

      })

      We need to define error-handling middleware last, after other app.use() and routes calls. The example is shown below – 

      app.get('/', (req, res, next) => {

       req.foo = true;

        setTimeout(() => {

          try {

            throw new Error('error');

          }

          catch (ex) {

            next(ex);

          }

        })

      });

      app.use((err, req, res, next) => {

        if (req.foo) {

          res.status(500).send('Fail!');

        }

        else {

          next(err);

        }

      })

      app.use((err, req, res, next) => {

        res.status(500).send('Error!')

      })

    6th Nov 2019

    I had a hard time learning how to handle errors in Express when I started. Nobody seemed to have written the answers I needed, so I had to learn it the hard way.

    Today, I want to share everything I know about handling errors in an Express app.

    Let’s begin with synchronous errors.

    Handling synchronous errors

    If you want to handle a synchronous error, you can throw the error in an Express request handler. (Note: Request handlers are also called controllers. I prefer saying request handlers because they’re explicit and easy to understand).

    app.post('/testing', (req, res) => {
      throw new Error('Something broke! 😱')
    })
    

    These errors can be caught with an Express error handler. If you did not write a custom error handler (more on this below), Express will handle the error for you with a default error handler.

    Express’s default error handler will:

    1. Set the HTTP Status to 500
    2. Sends a text response to the requester
    3. Logs the text response in the console

    Error returns to the client

    Handling asynchronous errors

    If you want to handle an asynchronous error, you need to send the error into an express error handler through the next argument.

    app.post('/testing', async (req, res, next) => {
      return next(new Error('Something broke again! 😱'))
    })
    

    If you’re using Async/await in an Express app, you want to use a wrapper function like express-async-handler. This lets you write asynchronous code without try/catch blocks. I wrote more about this in “Using Async/await in Express”.

    const asyncHandler = require('express-async-handler')
    
    app.post(
      '/testing',
      asyncHandler(async (req, res, next) => {
        // Do something
      })
    )
    

    Once you wrapped the request handler with express-async-handler, you can throw the error as before, and it’ll be handled with an Express error handler.

    app.post(
      '/testing',
      asyncHandler(async (req, res, next) => {
        throw new Error('Something broke yet again! 😱')
      })
    )
    

    Writing a custom error handler

    Express error handlers take in four arguments:

    1. error
    2. req
    3. res
    4. next

    They must be placed after all your middlewares and routes.

    app.use(/*...*/)
    app.get(/*...*/)
    app.post(/*...*/)
    app.put(/*...*/)
    app.delete(/*...*/)
    
    // Place your error handler after all other middlewares
    app.use((error, req, res, next) => {
      /* ... */
    })
    

    Express will stop using its default error handler once you create a custom error handler. To handle an error, you need to communicate with the frontend that’s requesting the endpoint. This means you need to:

    1. Send over a valid HTTP status code
    2. Send over a valid response

    A valid HTTP status code depends on what happened. Here’s a list of common errors you should prepare for:

    1. 400 Bad Request Error:
    • Used when user fails to include a field (like no credit card information in a payment form)
    • Also used when user enters incorrect information (Example: Entering different passwords in a password field and password confirmation field).
    1. 401 Unauthorized Error: Used when user enters incorrect login information (like username, email or password).
    2. 403 Forbidden Error: Used when user is not allowed access the endpoint.
    3. 404 Not Found Error: Used when the endpoint cannot be found.
    4. 500 Internal Server Error: Used the request sent by the frontend is correct, but there was an error from the backend.

    Once you determined the correct HTTP status code, you want to set the status with res.status

    app.use((error, req, res, next) => {
      // Bad request error
      res.status(400)
      res.json(/* ... */)
    })
    

    The HTTP status code should match the error message. For the status code to match the error message, you must send the status code together with the error.

    The easiest way is to use the http-errors package. It lets you send three things in your errors:

    1. A status code
    2. A message to go with the error
    3. Any properties you’d like to send. This is optional.

    Installing http-errors:

    npm install http-errors --save
    

    Using http-errors:

    const createError = require('http-errors')
    
    // Creating an error
    throw createError(status, message, properties)
    

    Let’s work through an example together to make it clearer. Let’s say you tried to find a user by their email address. The user cannot be found. You want to throw an error that says “User not found”.

    When you create the error, you want to:

    1. Send a 400 Bad Request Error (because the user filled in incorrect information). You send this as the first parameter.
    2. Send a message that says “User not found”. You send this as the second parameter.
    app.put(
      '/testing',
      asyncHandler(async (req, res) => {
        const { email } = req.body
        const user = await User.findOne({ email })
    
        // Throws error if user not found
        if (!user) throw createError(400, `User '${email}' not found`)
      })
    )
    

    You can get the status code with error.status and the error message with error.message.

    // Logging the error
    app.use((error, req, res, next) => {
      console.log('Error status: ', error.status)
      console.log('Message: ', error.message)
    })
    

    Status code and error message logged into the console.

    Then, you set the error status with res.status. You send the message with res.json.

    app.use((error, req, res, next) => {
      // Sets HTTP status code
      res.status(error.status)
    
      // Sends response
      res.json({ message: error.message })
    })
    

    Personally I like to send the status, the message, and the stack trace for me to debug easily.

    app.use((error, req, res, next) => {
      // Sets HTTP status code
      res.status(error.status)
    
      // Sends response
      res.json({
        status: error.status,
        message: error.message,
        stack: error.stack
      })
    })
    

    Fallback status code

    If the error did not originate from createError, it will not have a status property.

    Here’s an example. Let’s say you tried to read a file with fs.readFile, but the file does not exist.

    const fs = require('fs')
    const util = require('util')
    
    // Converts readFile from callbacks to Async/await.
    // Find out how to do this here: https://zellwk.com/blog/converting-callbacks-to-promises/
    const readFilePromise = util.promisify(fs.readFile)
    
    app.get('/testing', asyncHandler(async (req, res, next) => {
      const data = await readFilePromise('some-file')
    })
    

    This error would not contain a status property.

    app.use((error, req, res, next) => {
      console.log('Error status: ', error.status)
      console.log('Message: ', error.message)
    })
    

    Error does not contain the status property

    In these cases, you can default to 500 Internal Server Error.

    app.use((error, req, res, next) => {
      res.status(error.status || 500)
      res.json({
        status: error.status,
        message: error.message,
        stack: error.stack
      })
    })
    

    Changing an error’s status code

    Let’s say you want to retrieve a file from a user’s input. If the file does not exist, you should throw a 400 Bad Request Error, because it’s not your server’s fault.

    In this case, you want to use try/catch to catch the original error. Then, you recreate an error with createError.

    app.get('/testing', asyncHandler(async (req, res, next) => {
      try {
        const { file } = req.body
        const contents = await readFilePromise(path.join(__dirname, file))
      } catch (error) {
        throw createError(400, `File ${file} does not exist`)
      }
    })
    

    Handling 404 errors

    An endpoint is not found if a request falls through all your middlewares and routes.

    To handle a Not Found Error, you insert a middleware between your routes and your error handler. Here, create an error with createError.

    // Middlewares...
    // Routes...
    
    app.use((req, res, next) => {
      next(createError(404))
    })
    
    // Error handler...
    

    Not found error sent to the client.

    Don’t panic if you see an error that says “Cannot set headers after they’re sent to the server”.

    Error: Cannot set headers after they're sent.

    This error happens because the code ran methods that set response headers more than once in the same handler. These are the methods that set a response headers for you:

    1. res.send
    2. res.json
    3. res.render
    4. res.sendFile
    5. res.sendStatus
    6. res.end
    7. res.redirect

    For example, if you run res.render and res.json in the same response handler, you will get the “Cannot set headers after they’re sent” error.

    app.get('/testing', (req, res) => {
      res.render('new-page')
      res.json({ message: '¯_(ツ)_/¯' })
    })
    

    So, if you get this error, double-check your response handlers to make it doesn’t run the above methods twice.

    When streaming

    If an error occurs when you’re streaming a response to the frontend, you will get the same “Cannot set headers” error.

    In this case, Express states you should delegate the error handling to the default Express handlers. It will send an error and close the connection for you.

    app.use((error, req, res, next) => {
      // Do this only if you're streaming a response
      if (res.headersSent) {
        return next(error)
      }
    
      // Rest of the error handlers
    })
    

    That’s all I know for now! :)

    If you enjoyed this article, please support me by sharing this article Twitter or buying me a coffee 😉. If you spot a typo, I’d appreciate if you can correct it on GitHub. Thank you!

    Error handling is a crucial part of any production-ready application. It’s often neglected when you are exploring project ideas and trying to learn. But you should never forget about it when you launch your project for public use.

    Your users deserve to have the best experience and receive useful error messages. And you deserve the peace of mind that your application can handle all kinds of errors.

    When you decide to handle errors yourself, you can add useful information to them, such as HTTP response status codes. You can separate critical errors from those caused by users. Doing error handling yourself gives you options like error logging or sending yourself an email.

    In this post you will learn about the many parts of error handling, such as the following:

    • catching all types of errors
    • funneling all errors into a single error handler
    • creating your own error-handling middleware
    • setting up a custom error class
    • implementing the error handler to process all errors

    To just see the code, visit this demo repository.

    Prerequisites

    You should have an Express application set up with TypeScript.

    These are the dependencies used in this post.

    npm i express
    npm i @types/express @types/node ts-node-dev typescript --save-dev
    

    Catching Errors

    To handle errors, you first must catch them. You could program the best error handler, but it wouldn’t matter if some errors would escape it.

    Express can catch all synchronous errors and send them to its error-handling middleware.

    To verify, try throwing an error in one of your routes.

    router.get('/', (req: Request, res: Response) => {
      throw new Error('This is an error');
    
      res.json({ status: 'ok' });
    });
    

    Visit the / route in your browser and you should see an error.

    Error: This is an error
        at /path/to/project/src/routes.ts:6:9
        ...
    

    Express includes the stack trace in the error message you see in your browser. If you set Node environment to production (NODE_ENV=production), Express will hide the stack trace.

    However, errors thrown in asynchronous code can go unnoticed by Express.

    Catching Errors in Asynchronous Code

    As just mentioned, Express doesn’t catch errors thrown in asynchronous code. Unless you are from the future and using Express 5. You can skip this section then.

    For those stuck with Express 4, consider the following code that throws an error inside a promise. Something that could happen when reading user data from a database.

    const getUserFromDb = () => {
      return new Promise(() => {
        throw new Error('This is an error');
      });
    };
    
    router.get('/', async (req: Request, res: Response) => {
      const data = await getUserFromDb();
    
      res.json({ user: data });
    });
    

    If you revisit the / route, the page won’t load and you will need to restart your application.

    You need to catch errors in async code yourself. Otherwise Express won’t pass them to the default error-handling middleware. You can use try/catch and the NextFunction to do that.

    import { NextFunction, Request, Response } from 'express';
    
    router.get('/', async (req: Request, res: Response, next: NextFunction) => {
      try {
        const data = await getUserFromDb();
    
        res.json({ user: data });
      } catch (error) {
        next(error);
      }
    });
    

    Notice how you pass the caught error to next function. Any argument you pass to next, Express will treat as an error. By calling next(error), you jump from any middleware straight to the error-handling middleware.

    The problem with try/catch approach is having to do it tens or hundreds of times as your app grows. It is repetitive, hence error-prone.

    You can use a package dedicated to catching async errors for you. One such package is express-async-errors.

    Install it as a dependency.

    npm i express-async-errors
    

    And import it before you register any router handlers.

    // src/routes.ts
    
    import 'express-async-errors';
    import { NextFunction, Request, Response, Router } from 'express';
    
    const router = Router();
    
    const getUserFromDb = () => {
      return new Promise(() => {
        throw new Error('This is an error');
      });
    };
    
    router.get('/', async (req: Request, res: Response) => {
      const data = await getUserFromDb();
    
      res.json({ user: data });
    });
    
    export default router;
    

    The express-async-errors package will make sure async errors in your routes get caught. And Express will be able to handle these errors.

    So far you have learned that Express can catch errors in synchronous code and how to help it catch async errors. What about errors that Express doesn’t notice? You need to deal with unhandled rejections and uncaught exceptions yourself.

    Dealing With Unhandled and Uncaught Errors

    Sometimes an error goes unnoticed and Express doesn’t handle it. When Node.js encounters such errors, it emits events that you can listen to.

    The first event you need to listen to is unhandledRejection. It happens when your code doesn’t handle a rejected Promise with a .catch().

    const getUserFromDb = async () => {
      return new Promise((_, reject) => {
        reject('This is an error');
      });
    };
    
    router.get('/', async (req: Request, res: Response) => {
      getUserFromDb()
        .then(value => {
          res.json(value);
        })
    });
    

    Since this code doesn’t handle rejections from getUserFromDb with a .catch(), an UnhandledPromiseRejection warning shows up in Node.js console.

    To fix it, you can register a listener function on the process object that handles the unhandledRejection event.

    // src/process.ts
    
    process.on('unhandledRejection', (reason: Error | any) => {
      console.log(`Unhandled Rejection: ${reason.message || reason}`);
    
      throw new Error(reason.message || reason);
    });
    

    Since rejections are used to return errors from Promises, you can throw an error with the given reason from the rejection.

    In this case, Express won’t handle the thrown error anyway. You have created an uncaught exception. Any errors in your code base, that Express can’t handle, turn into uncaught exceptions. Luckily, you can catch them errors yourself.

    To catch uncaught exceptions, listen to the uncaughtException event by setting up an event listener on the process object.

    // src/process.ts
    
    process.on('uncaughtException', (error: Error) => {
      console.log(`Uncaught Exception: ${error.message}`);
    
      errorHandler.handleError(error);
    });
    

    Here you want to funnel the errors into a function that handles them. Later in this post you will learn how to implement the handleError function.

    You can crash your application by calling process.exit(1). Set up an automatic restart mechanism when your application exits with a non-zero code.

    Don’t forget to import the code that registers the process event listener functions into your app.

    // index.ts
    
    import express, { Application } from 'express';
    import './src/process';
    

    You learned how to funnel uncaught errors into your own error handler. The errors caught in middleware functions are still handled by Express. If you want to handle them yourself, you need to create your own error-handling middleware.

    Creating a Custom Error-Handling Middleware

    To override default Express error responses, you need to create your own error-handling middleware.

    An error-handling middleware differs from other middleware functions by having 4 parameters — err, req, res, next. Additionally, it has to be the last middleware you set up in your application. For this reason, call app.use(router) after all other app.use() calls in your index.ts. And register the error-handling middleware after registering other route handlers.

    Now go ahead and register your custom error-handling middleware.

    // src/routes.ts
    
    import { NextFunction, Request, Response, Router } from 'express';
    import { errorHandler } from './exceptions/ErrorHandler';
    
    // ... all other routes
    
    router.use((err: Error, req: Request, res: Response, next: NextFunction) => {
      errorHandler.handleError(err, res);
    });
    

    Similarly to how you previously handled uncaught exceptions, you can funnel all errors into errorHandler, which you will get to implement very soon. You should also pass the Response object to the error handler so you can use it to send a response.

    You can have more than one error-handling middleware. To pass the error from one to another, call the next function and pass it the error.

    // ... all other routes
    
    router.use((err: Error, req: Request, res: Response, next: NextFunction) => {
      // 1. Log the error or send it to a 3rd party error monitoring software
      logger.logError(err);
    
      next(err);
    });
    
    router.use((err: Error, req: Request, res: Response, next: NextFunction) => {
      // 2. Send an email to yourself, or a message somewhere
      messenger.sendErrorMessage(err);
    
      next(err);
    });
    
    router.use((err: Error, req: Request, res: Response, next: NextFunction) => {
      // 3. Lastly, handle the error
      errorHandler.handleError(err, res);
    });
    

    The error will flow through the error-handling middleware functions from first to last (1-2-3).

    The last error-handling middleware should send a response, so the client’s connection doesn’t hang up. In this case, handleError should send a response through the res argument.

    You can also call next(err) in your last error-handling middleware to send it to Express error handler.

    Good job, you have learned a lot. You have set up an error-handling middleware that funnels the caught errors into your error handler. Funneling both uncaught and caught errors, you are almost ready to handle them. One last thing you should do is create a custom error class. This will help to determine the severity of an error and what the HTTP response status code should be.

    Creating Custom Error Class

    You can use a custom error class to differentiate errors from one another. You might want to add data to an error or handle it differently than other types of errors.

    One improvement you can do is attaching an HTTP response status code to your errors.

    You can use an enum to map status codes to a human readable name. This way you don’t have to remember the numbers.

    // exceptions/AppError.ts
    
    export enum HttpCode {
      OK = 200,
      NO_CONTENT = 204,
      BAD_REQUEST = 400,
      UNAUTHORIZED = 401,
      NOT_FOUND = 404,
      INTERNAL_SERVER_ERROR = 500,
    }
    

    These are some of the most common status codes. Go ahead and add any other ones your application uses. You can use this MDN article as a reference.

    Another improvement is separating critical application errors from those that are expected. For example, validation errors caused by user input are fine. Errors thrown from mistakes made by you, the developer, are bad.

    Now go and create your own AppError class that extends the default Error class.

    // exceptions/AppError.ts
    
    export enum HttpCode { /*...*/ }
    
    interface AppErrorArgs {
      name?: string;
      httpCode: HttpCode;
      description: string;
      isOperational?: boolean;
    }
    
    export class AppError extends Error {
      public readonly name: string;
      public readonly httpCode: HttpCode;
      public readonly isOperational: boolean = true;
    
      constructor(args: AppErrorArgs) {
        super(args.description);
    
        Object.setPrototypeOf(this, new.target.prototype);
    
        this.name = args.name || 'Error';
        this.httpCode = args.httpCode;
    
        if (args.isOperational !== undefined) {
          this.isOperational = args.isOperational;
        }
    
        Error.captureStackTrace(this);
      }
    }
    

    When you use super in a child class, you call the constructor of parent class. In this case, calling super will trigger Error class constructor, which sets the error’s message property to contain your description.

    The required httpCode and description is what your application will return in responses. Optionally, you can give your error a name.

    The isOperational property is what determines if this error is a serious mistake. Setting it to true means that the error is normal and the user should receive an explanation what caused it.

    Any error that is not operational indicates a problem with your code, which you should investigate and fix.

    Using the Custom Error Class

    You use your AppError class whenever you want to fail the client’s request. Whether it be because user lacks permissions, their input is invalid, or they are not logged in. Or for any other reason you desire.

    import { AppError, HttpCode } from './exceptions/AppError';
    
    router.get('/user/:id', async (req: Request, res: Response) => {
      if (!res.locals.user) {
        throw new AppError({
          httpCode: HttpCode.UNAUTHORIZED,
          description: 'You must be logged in',
        });
      }
    
      const user = await getUserFromDb();
    
      if (!user) {
        throw new AppError({
          httpCode: HttpCode.NOT_FOUND,
          description: 'User you are looking for does not exist',
        });
      }
    
      res.json(user);
    });
    

    You are not limited to routes, you can throw AppError anywhere in your code. Remember to set isOperational to false when throwing a critical error.

    Your error-handling middleware that you created earlier will catch all errors from your routes. It will then send them to your error handler, which you are now finally going to create.

    Creating an Error Handler

    Your error handler should distinguish errors that can be trusted. A trusted error doesn’t take much work, you just have to send an error response to the client. On the other hand, an error you can’t trust requires extra steps.

    Start by creating an ErrorHandler class that can determine if an error can be trusted.

    // exceptions/ErrorHandler.ts
    
    import { Response } from 'express';
    import { AppError, HttpCode } from './AppError';
    
    class ErrorHandler {
      private isTrustedError(error: Error): boolean {
        if (error instanceof AppError) {
          return error.isOperational;
        }
    
        return false;
      }
    }
    
    export const errorHandler = new ErrorHandler();
    

    You can’t trust any other error than your custom AppError. On top of that, if you set isOperational to false when throwing an AppError, you can’t trust such error either.

    Now you should handle trustworthy errors coming into your error handler separately from the rest. Check if the error is trustworthy and send it to its dedicated function. Otherwise send it to a function for critical errors.

    class ErrorHandler {
      private isTrustedError(error: Error): boolean { /* ...  */ }
    
      public handleError(error: Error | AppError, response?: Response): void {
        if (this.isTrustedError(error) && response) {
          this.handleTrustedError(error as AppError, response);
        } else {
          this.handleCriticalError(error, response);
        }
      }
    }
    

    If an error is trustworthy, it is an instance of AppError, so you can pass handleTrustedError an argument of error as AppError. Since trusted errors can only come from your error-handling middleware, your error handler always receives the Response object along with the error. So, pass it to handleTrustedError as well.

    In case of untrustworthy errors, you will check if the response is defined. Because these errors can come from outside the request-response cycle. For example, when you handle an uncaught exception.

    Handle a trusted error by sending the client a response with the HTTP status code and a message.

    class ErrorHandler {
      // ...
    
      private handleTrustedError(error: AppError, response: Response): void {
        response.status(error.httpCode).json({ message: error.message });
      }
    }
    

    This is where you could also pass the name property to json(). Naming errors can be useful if you want to translate the error message on the client.

    On the other hand, untrustworthy errors are dangerous, because they can make your application behave unexpectedly. Based on Node.js best practices on error handling, you should crash your application when you catch such error.

    class ErrorHandler {
      // ...
    
      private handleCriticalError(error: Error | AppError, response?: Response): void {
        if (response) {
          response
            .status(HttpCode.INTERNAL_SERVER_ERROR)
            .json({ message: 'Internal server error' });
        }
    
        console.log('Application encountered a critical error. Exiting');
        process.exit(1);
      }
    }
    

    Since untrustworthy errors come from outside the error-handling middleware, you should check if Response is available. If it’s defined, send a generic server error message to the client.

    This is where you would benefit from an automatic restart setup. If your application exits with a non-zero code, it should restart by itself, so you can crash on critical errors without much downtime.

    I have written a post Graceful Shutdown in Express that explains how to create your own exit handler.

    Of course, you can do much more than just exiting your application. You can set up error logging or notify yourself via email or a messaging service. You could also improve the shutdown procedure to be graceful, stopping all HTTP connections before the exit.

    Summary

    You are now knowledgeable of what goes into error handling in Express. Here’s a recap of the things you’ve learned:

    • You need to use express-async-errors, because Express can’t catch async errors on its own.
    • You can catch unhandled rejections and uncaught exceptions by listening to events with process.on.
    • You can customize error handling by making your own error-handling middleware.
    • You should use custom error class to save more information, such as status codes and trustworthiness of the error.
    • You should treat untrustworthy errors seriously and probably just restart your application.

    You can see the code in this demo repository.

    Понравилась статья? Поделить с друзьями:
  • Node js error stack trace
  • Node js error socket hang up
  • Node js error object
  • Node js error message
  • Node js error cannot find module express