error.js

// *****************************************************************************
// Copyright 2013-2023 Aerospike, Inc.
//
// 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 status = require('./status')

/**
 *
 *
 * @extends Error
 * @class AerospikeError
 * @classdesc Error raised by the client when execution of a database command fails. This
 * may be either due to an error status code returned by the server, or caused
 * by an error condition that occured on the client side.
 *
 * @example <caption>Expected output: "Error: 127.0.0.1:3000 Record does not exist in database. May be returned by read, or write with policy Aerospike.policy.exists.UPDATE [2]"</caption>
 *
 * const Aerospike = require('aerospike')
 *
 * // INSERT HOSTNAME AND PORT NUMBER OF AEROSPIKE SERVER NODE HERE!
 * var config = {
 *   hosts: '192.168.33.10:3000',
 *   // Timeouts disabled, latency dependent on server location. Configure as needed.
 *   policies: {
 *     read : new Aerospike.ReadPolicy({socketTimeout : 0, totalTimeout : 0}),
 *     write : new Aerospike.WritePolicy({socketTimeout : 0, totalTimeout : 0})
 *   }
 * }
 *
 * let key = new Aerospike.Key('test', 'key', 'does_not_exist')
 * Aerospike.connect()
 *   .then(client => {
 *     client.get(key)
 *       .then(record => console.info(record))
 *       .catch(error => console.error(`Error: ${error.message} [${error.code}]`))
 *       .then(() => client.close())
 *   })
 */
class AerospikeError extends Error {
  constructor (message, command) {
    super(message)
    this.name = this.constructor.name

    /**
     * Numeric status code returned by the server or the client.
     *
     * @type {number}
     *
     * @see {@link module:aerospike.status} contains the full list of possible status codes.
     */
    this.code = status.ERR_CLIENT

    /**
     * Command during which the error occurred.
     *
     * @type {?Object}
     */
    this.command = command || null

    /**
     * C/C++ function name in which the error occurred.
     *
     * @type {?string}
     */
    this.func = null

    /**
     * File name of the C/C++ source file in which the error occurred.
     *
     * @type {?string}
     */
    this.file = null

    /**
     * Line number in the C/C++ source file in which the error occurred.
     *
     * @type {?number}
     */
    this.line = null

    /**
     * It is possible that a write transaction completed even though the client
     * returned this error. This may be the case when a client error occurs
     * (like timeout) after the command was sent to the server.
     *
     * @type {boolean}
     */
    this.inDoubt = false

    if (command && command.stack) {
      this.setStackTrace(command.stack)
    }
  }

  /** @private */
  static fromASError (asError, command) {
    if (!asError) {
      return null
    } else if (asError.code === status.OK) {
      return null
    } else if (asError instanceof AerospikeError) {
      return asError
    } else {
      const message = this.formatMessage(asError.message, asError.code)
      const error = new AerospikeError(message, command)
      this.copyASErrorProperties(error, asError)
      return error
    }
  }

  /** @private */
  static copyASErrorProperties (target, source) {
    target.code = source.code
    target.inDoubt = source.inDoubt
    target.func = source.func
    target.file = source.file
    target.line = Number.parseInt(source.line)
  }

  /** @private */
  static formatMessage (message, code) {
    if (message) {
      message = message.replace(/AEROSPIKE_[A-Z_]+/, () => status.getMessage(code))
    }
    return message
  }

  /** @private */
  setStackTrace (stack) {
    const firstLine = `${this.name}: ${this.message}`
    stack = stack.replace(/^.*$/m, firstLine)
    Object.defineProperty(this, 'stack', { value: stack })
  }

  /**
   * Indicates whether the error originated on the database server.
   *
   * @returns {boolean} - <code>true</code> if the server raised the error, <code>false</code> otherwise.
   */
  isServerError () {
    return this.code > status.OK
  }

  /**
   * The {@link Client} instance associated with this error, if any.
   *
   * @type {?Client}
   * @since v3.7.0
   *
   * @example <caption>Closing the client connection, when an error occurs:</caption>
   *
   * const Aerospike = require('aerospike')
   *
   * // INSERT HOSTNAME AND PORT NUMBER OF AEROSPIKE SERVER NODE HERE!
   * var config = {
   *   hosts: '192.168.33.10:3000',
   *   // Timeouts disabled, latency dependent on server location. Configure as needed.
   *   policies: {
   *     read : new Aerospike.ReadPolicy({socketTimeout : 0, totalTimeout : 0}),
   *     write : new Aerospike.WritePolicy({socketTimeout : 0, totalTimeout : 0})
   *   }
   * }
   *
   * Aerospike.connect(config).then(async client => {
   *   await client.put(new Aerospike.Key('demo', 'test', 'foo'), { 'foo': 'bar' })
   *   client.close()
   * }).catch(error => {
   *   console.error('Error: %s [%i]', error.message, error.code)
   *   if (error.client) error.client.close()
   * })
   */
  get client () {
    if (this.command) return this.command.client
  }
}

module.exports = AerospikeError