/* * Copyright 2016 resin.io * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ 'use strict' const _ = require('lodash') /** * @summary Create an error details object * @function * @private * * @param {Object} options - options * @param {(String|Function)} options.title - error title * @param {(String|Function)} options.description - error description * @returns {Object} error details object * * @example * const details = createErrorDetails({ * title: (error) => { * return `An error happened, the code is ${error.code}`; * }, * description: 'This is the error description' * }); */ const createErrorDetails = (options) => { return _.pick(_.mapValues(options, (value) => { return _.isFunction(value) ? value : _.constant(value) }), [ 'title', 'description' ]) } /** * @summary Human-friendly error messages * @namespace HUMAN_FRIENDLY * @public */ exports.HUMAN_FRIENDLY = { /* eslint-disable new-cap */ /** * @namespace ENOENT * @memberof HUMAN_FRIENDLY */ ENOENT: createErrorDetails({ title: (error) => { return `No such file or directory: ${error.path}` }, description: 'The file you\'re trying to access doesn\'t exist' }), /** * @namespace EPERM * @memberof HUMAN_FRIENDLY */ EPERM: createErrorDetails({ title: 'You\'re not authorized to perform this operation', description: 'Please ensure you have necessary permissions for this task' }), /** * @namespace EACCES * @memberof HUMAN_FRIENDLY */ EACCES: createErrorDetails({ title: 'You don\'t have access to this resource', description: 'Please ensure you have necessary permissions to access this resource' }), /** * @namespace ENOMEM * @memberof HUMAN_FRIENDLY */ ENOMEM: createErrorDetails({ title: 'Your system ran out of memory', description: 'Please make sure your system has enough available memory for this task' }) /* eslint-enable new-cap */ } /** * @summary Get user friendly property from an error * @function * @private * * @param {Error} error - error * @param {String} property - HUMAN_FRIENDLY property * @returns {(String|Undefined)} user friendly message * * @example * const error = new Error('My error'); * error.code = 'ENOMEM'; * * const friendlyDescription = getUserFriendlyMessageProperty(error, 'description'); * * if (friendlyDescription) { * console.log(friendlyDescription); * } */ const getUserFriendlyMessageProperty = (error, property) => { const code = _.get(error, [ 'code' ]) if (_.isNil(code) || !_.isString(code)) { return null } return _.invoke(exports.HUMAN_FRIENDLY, [ code, property ], error) } /** * @summary Check if a string is blank * @function * @private * * @param {String} string - string * @returns {Boolean} whether the string is blank * * @example * if (isBlank(' ')) { * console.log('The string is blank'); * } */ const isBlank = _.flow([ _.trim, _.isEmpty ]) /** * @summary Get the title of an error * @function * @public * * @description * Try to get as much information as possible about the error * rather than falling back to generic messages right away. * * @param {Error} error - error * @returns {String} error title * * @example * const error = new Error('Foo bar'); * const title = errors.getTitle(error); * console.log(title); */ exports.getTitle = (error) => { if (!_.isError(error) && !_.isPlainObject(error) && !_.isNil(error)) { return _.toString(error) } const codeTitle = getUserFriendlyMessageProperty(error, 'title') if (!_.isNil(codeTitle)) { return codeTitle } const message = _.get(error, [ 'message' ]) if (!isBlank(message)) { return message } const code = _.get(error, [ 'code' ]) if (!_.isNil(code) && !isBlank(code)) { return `Error code: ${code}` } return 'An error ocurred' } /** * @summary Get the description of an error * @function * @public * * @param {Error} error - error * @param {Object} options - options * @param {Boolean} [options.userFriendlyDescriptionsOnly=false] - only return user friendly descriptions * @returns {String} error description * * @example * const error = new Error('Foo bar'); * const description = errors.getDescription(error); * console.log(description); */ exports.getDescription = (error, options = {}) => { _.defaults(options, { userFriendlyDescriptionsOnly: false }) if (!_.isError(error) && !_.isPlainObject(error)) { return '' } if (!isBlank(error.description)) { return error.description } const codeDescription = getUserFriendlyMessageProperty(error, 'description') if (!_.isNil(codeDescription)) { return codeDescription } if (options.userFriendlyDescriptionsOnly) { return '' } if (error.stack) { return error.stack } if (_.isEmpty(error)) { return '' } const INDENTATION_SPACES = 2 return JSON.stringify(error, null, INDENTATION_SPACES) } /** * @summary Create an error * @function * @public * * @param {Object} options - options * @param {String} options.title - error title * @param {String} [options.description] - error description * @param {Boolean} [options.report] - report error * @returns {Error} error * * @example * const error = errors.createError({ * title: 'Foo' * description: 'Bar' * }); * * throw error; */ exports.createError = (options) => { if (isBlank(options.title)) { throw new Error(`Invalid error title: ${options.title}`) } const error = new Error(options.title) error.description = options.description if (!_.isNil(options.report) && !options.report) { error.report = false } if (!_.isNil(options.code)) { error.code = options.code } return error } /** * @summary Create a user error * @function * @public * * @description * User errors represent invalid states that the user * caused, that are not errors on the application itself. * Therefore, user errors don't get reported to analytics * and error reporting services. * * @param {Object} options - options * @param {String} options.title - error title * @param {String} [options.description] - error description * @returns {Error} user error * * @example * const error = errors.createUserError({ * title: 'Foo', * description: 'Bar' * }); * * throw error; */ exports.createUserError = (options) => { return exports.createError({ title: options.title, description: options.description, report: false, code: options.code }) } /** * @summary Check if an error is an user error * @function * @public * * @param {Error} error - error * @returns {Boolean} whether the error is a user error * * @example * const error = errors.createUserError('Foo', 'Bar'); * * if (errors.isUserError(error)) { * console.log('This error is a user error'); * } */ exports.isUserError = (error) => { return _.isNil(error.report) ? false : !error.report } /** * @summary Convert an Error object to a JSON object * @function * @public * * @param {Error} error - error object * @returns {Object} json error * * @example * const error = errors.toJSON(new Error('foo')) * * console.log(error.message); * > 'foo' */ exports.toJSON = (error) => { // Handle string error objects to be on the safe side const isErrorLike = _.isError(error) || _.isPlainObject(error) const errorObject = isErrorLike ? error : new Error(error) return { name: errorObject.name, message: errorObject.message, description: errorObject.description, stack: errorObject.stack, report: errorObject.report, code: errorObject.code, syscall: errorObject.syscall, errno: errorObject.errno, stdout: errorObject.stdout, stderr: errorObject.stderr, device: errorObject.device } } /** * @summary Convert a JSON object to an Error object * @function * @public * * @param {Error} json - json object * @returns {Object} error object * * @example * const error = errors.fromJSON(errors.toJSON(new Error('foo'))); * * console.log(error.message); * > 'foo' */ exports.fromJSON = (json) => { return _.assign(new Error(json.message), json) }