// Copyright (c) 2025 Mahmoud Alghalayini. All rights reserved.
//
// 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';
/**
* Base error class for all rclnodejs errors.
* Provides structured error information with context.
* @class
*/
class RclNodeError extends Error {
/**
* @param {string} message - Human-readable error message
* @param {object} [options] - Additional error context
* @param {string} [options.code] - Machine-readable error code (e.g., 'TIMEOUT', 'INVALID_ARGUMENT')
* @param {string} [options.nodeName] - Name of the node where error occurred
* @param {string} [options.entityType] - Type of entity (publisher, subscription, client, etc.)
* @param {string} [options.entityName] - Name of the entity (topic name, service name, etc.)
* @param {Error} [options.cause] - Original error that caused this error
* @param {any} [options.details] - Additional error-specific details
*/
constructor(message, options = {}) {
super(message);
// Maintains proper stack trace for where our error was thrown
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.code = options.code || 'UNKNOWN_ERROR';
this.nodeName = options.nodeName;
this.entityType = options.entityType;
this.entityName = options.entityName;
this.details = options.details;
// Error chaining (ES2022 feature, Node.js 16.9+)
if (options.cause) {
this.cause = options.cause;
}
// Timestamp for logging/debugging
this.timestamp = new Date();
}
/**
* Returns a detailed error object for logging/serialization
* @return {object} Structured error information
*/
toJSON() {
return {
name: this.name,
message: this.message,
code: this.code,
nodeName: this.nodeName,
entityType: this.entityType,
entityName: this.entityName,
details: this.details,
timestamp: this.timestamp.toISOString(),
stack: this.stack,
cause: this.cause
? this.cause.toJSON?.() || this.cause.message
: undefined,
};
}
/**
* Returns a user-friendly error description
* @return {string} Formatted error string
*/
toString() {
let str = `${this.name}: ${this.message}`;
if (this.code) str += ` [${this.code}]`;
if (this.nodeName) str += ` (node: ${this.nodeName})`;
if (this.entityName)
str += ` (${this.entityType || 'entity'}: ${this.entityName})`;
return str;
}
}
/**
* Error thrown when validation fails
* @class
* @extends RclNodeError
*/
class ValidationError extends RclNodeError {
/**
* @param {string} message - Error message
* @param {object} [options] - Additional options
* @param {string} [options.argumentName] - Name of the argument that failed validation
* @param {any} [options.providedValue] - The value that was provided
* @param {string} [options.expectedType] - The expected type or format
* @param {string} [options.validationRule] - The validation rule that failed
*/
constructor(message, options = {}) {
super(message, { code: 'VALIDATION_ERROR', ...options });
this.argumentName = options.argumentName;
this.providedValue = options.providedValue;
this.expectedType = options.expectedType;
this.validationRule = options.validationRule;
}
}
/**
* Type validation error
* @class
* @extends ValidationError
*/
class TypeValidationError extends ValidationError {
/**
* @param {string} argumentName - Name of the argument
* @param {any} providedValue - The value that was provided
* @param {string} expectedType - The expected type
* @param {object} [options] - Additional options
*/
constructor(argumentName, providedValue, expectedType, options = {}) {
super(
`Invalid type for '${argumentName}': expected ${expectedType}, got ${typeof providedValue}`,
{
code: 'INVALID_TYPE',
argumentName,
providedValue,
expectedType,
...options,
}
);
}
}
/**
* Range/value validation error
* @class
* @extends ValidationError
*/
class RangeValidationError extends ValidationError {
/**
* @param {string} argumentName - Name of the argument
* @param {any} providedValue - The value that was provided
* @param {string} constraint - The constraint that was violated
* @param {object} [options] - Additional options
*/
constructor(argumentName, providedValue, constraint, options = {}) {
super(
`Value '${providedValue}' for '${argumentName}' is out of range: ${constraint}`,
{
code: 'OUT_OF_RANGE',
argumentName,
providedValue,
validationRule: constraint,
...options,
}
);
}
}
/**
* ROS name validation error (topics, nodes, services)
* @class
* @extends ValidationError
*/
class NameValidationError extends ValidationError {
/**
* @param {string} name - The invalid name
* @param {string} nameType - Type of name (node, topic, service, etc.)
* @param {string} validationResult - The validation error message
* @param {number} invalidIndex - Index where validation failed
* @param {object} [options] - Additional options
*/
constructor(name, nameType, validationResult, invalidIndex, options = {}) {
super(
`Invalid ${nameType} name '${name}': ${validationResult}` +
(invalidIndex >= 0 ? ` at index ${invalidIndex}` : ''),
{
code: 'INVALID_NAME',
argumentName: nameType,
providedValue: name,
details: { validationResult, invalidIndex },
...options,
}
);
this.invalidIndex = invalidIndex;
this.validationResult = validationResult;
}
}
/**
* Base class for operation/runtime errors
* @class
* @extends RclNodeError
*/
class OperationError extends RclNodeError {
/**
* @param {string} message - Error message
* @param {object} [options] - Additional options
*/
constructor(message, options = {}) {
super(message, { code: 'OPERATION_ERROR', ...options });
}
}
/**
* Request timeout error
* @class
* @extends OperationError
*/
class TimeoutError extends OperationError {
/**
* @param {string} operationType - Type of operation that timed out
* @param {number} timeoutMs - Timeout duration in milliseconds
* @param {object} [options] - Additional options
*/
constructor(operationType, timeoutMs, options = {}) {
super(`${operationType} timeout after ${timeoutMs}ms`, {
code: 'TIMEOUT',
details: { timeoutMs, operationType },
...options,
});
this.timeout = timeoutMs;
this.operationType = operationType;
}
}
/**
* Request abortion error
* @class
* @extends OperationError
*/
class AbortError extends OperationError {
/**
* @param {string} operationType - Type of operation that was aborted
* @param {string} [reason] - Reason for abortion
* @param {object} [options] - Additional options
*/
constructor(operationType, reason, options = {}) {
super(`${operationType} was aborted` + (reason ? `: ${reason}` : ''), {
code: 'ABORTED',
details: { operationType, reason },
...options,
});
this.operationType = operationType;
this.abortReason = reason;
}
}
/**
* Service not available error
* @class
* @extends OperationError
*/
class ServiceNotFoundError extends OperationError {
/**
* @param {string} serviceName - Name of the service
* @param {object} [options] - Additional options
*/
constructor(serviceName, options = {}) {
super(`Service '${serviceName}' is not available`, {
code: 'SERVICE_NOT_FOUND',
entityType: 'service',
entityName: serviceName,
...options,
});
this.serviceName = serviceName;
}
}
/**
* Remote node not found error
* @class
* @extends OperationError
*/
class NodeNotFoundError extends OperationError {
/**
* @param {string} nodeName - Name of the node
* @param {object} [options] - Additional options
*/
constructor(nodeName, options = {}) {
super(`Node '${nodeName}' not found or not available`, {
code: 'NODE_NOT_FOUND',
entityType: 'node',
entityName: nodeName,
...options,
});
this.targetNodeName = nodeName;
}
}
/**
* Base error for parameter operations
* @class
* @extends RclNodeError
*/
class ParameterError extends RclNodeError {
/**
* @param {string} message - Error message
* @param {string} parameterName - Name of the parameter
* @param {object} [options] - Additional options
*/
constructor(message, parameterName, options = {}) {
super(message, {
code: 'PARAMETER_ERROR',
entityType: 'parameter',
entityName: parameterName,
...options,
});
this.parameterName = parameterName;
}
}
/**
* Parameter not found error
* @class
* @extends ParameterError
*/
class ParameterNotFoundError extends ParameterError {
/**
* @param {string} parameterName - Name of the parameter
* @param {string} nodeName - Name of the node
* @param {object} [options] - Additional options
*/
constructor(parameterName, nodeName, options = {}) {
super(
`Parameter '${parameterName}' not found on node '${nodeName}'`,
parameterName,
{
code: 'PARAMETER_NOT_FOUND',
nodeName,
...options,
}
);
}
}
/**
* Parameter type mismatch error
* @class
* @extends ParameterError
*/
class ParameterTypeError extends ParameterError {
/**
* @param {string} parameterName - Name of the parameter
* @param {string} expectedType - Expected parameter type
* @param {string} actualType - Actual parameter type
* @param {object} [options] - Additional options
*/
constructor(parameterName, expectedType, actualType, options = {}) {
super(
`Type mismatch for parameter '${parameterName}': expected ${expectedType}, got ${actualType}`,
parameterName,
{
code: 'PARAMETER_TYPE_MISMATCH',
details: { expectedType, actualType },
...options,
}
);
this.expectedType = expectedType;
this.actualType = actualType;
}
}
/**
* Read-only parameter modification error
* @class
* @extends ParameterError
*/
class ReadOnlyParameterError extends ParameterError {
/**
* @param {string} parameterName - Name of the parameter
* @param {object} [options] - Additional options
*/
constructor(parameterName, options = {}) {
super(
`Cannot modify read-only parameter '${parameterName}'`,
parameterName,
{
code: 'PARAMETER_READ_ONLY',
...options,
}
);
}
}
/**
* Base error for topic operations
* @class
* @extends RclNodeError
*/
class TopicError extends RclNodeError {
/**
* @param {string} message - Error message
* @param {string} topicName - Name of the topic
* @param {object} [options] - Additional options
*/
constructor(message, topicName, options = {}) {
super(message, {
code: 'TOPIC_ERROR',
entityType: 'topic',
entityName: topicName,
...options,
});
this.topicName = topicName;
}
}
/**
* Publisher-specific error
* @class
* @extends TopicError
*/
class PublisherError extends TopicError {
/**
* @param {string} message - Error message
* @param {string} topicName - Name of the topic
* @param {object} [options] - Additional options
*/
constructor(message, topicName, options = {}) {
super(message, topicName, {
code: 'PUBLISHER_ERROR',
entityType: 'publisher',
...options,
});
}
}
/**
* Subscription-specific error
* @class
* @extends TopicError
*/
class SubscriptionError extends TopicError {
/**
* @param {string} message - Error message
* @param {string} topicName - Name of the topic
* @param {object} [options] - Additional options
*/
constructor(message, topicName, options = {}) {
super(message, topicName, {
code: 'SUBSCRIPTION_ERROR',
entityType: 'subscription',
...options,
});
}
}
/**
* Base error for action operations
* @class
* @extends RclNodeError
*/
class ActionError extends RclNodeError {
/**
* @param {string} message - Error message
* @param {string} actionName - Name of the action
* @param {object} [options] - Additional options
*/
constructor(message, actionName, options = {}) {
super(message, {
code: 'ACTION_ERROR',
entityType: 'action',
entityName: actionName,
...options,
});
this.actionName = actionName;
}
}
/**
* Goal rejected by action server
* @class
* @extends ActionError
*/
class GoalRejectedError extends ActionError {
/**
* @param {string} actionName - Name of the action
* @param {string} goalId - ID of the rejected goal
* @param {object} [options] - Additional options
*/
constructor(actionName, goalId, options = {}) {
super(`Goal rejected by action server '${actionName}'`, actionName, {
code: 'GOAL_REJECTED',
details: { goalId },
...options,
});
this.goalId = goalId;
}
}
/**
* Action server not found
* @class
* @extends ActionError
*/
class ActionServerNotFoundError extends ActionError {
/**
* @param {string} actionName - Name of the action
* @param {object} [options] - Additional options
*/
constructor(actionName, options = {}) {
super(`Action server '${actionName}' is not available`, actionName, {
code: 'ACTION_SERVER_NOT_FOUND',
...options,
});
}
}
/**
* Wraps errors from native C++ layer with additional context
* @class
* @extends RclNodeError
*/
class NativeError extends RclNodeError {
/**
* @param {string} nativeMessage - Error message from C++ layer
* @param {string} operation - Operation that failed
* @param {object} [options] - Additional options
*/
constructor(nativeMessage, operation, options = {}) {
super(`Native operation failed: ${operation} - ${nativeMessage}`, {
code: 'NATIVE_ERROR',
details: { nativeMessage, operation },
...options,
});
this.nativeMessage = nativeMessage;
this.operation = operation;
}
}
module.exports = {
// Base error
RclNodeError,
// Validation errors
ValidationError,
TypeValidationError,
RangeValidationError,
NameValidationError,
// Operation errors
OperationError,
TimeoutError,
AbortError,
ServiceNotFoundError,
NodeNotFoundError,
// Parameter errors
ParameterError,
ParameterNotFoundError,
ParameterTypeError,
ReadOnlyParameterError,
// Topic errors
TopicError,
PublisherError,
SubscriptionError,
// Action errors
ActionError,
GoalRejectedError,
ActionServerNotFoundError,
// Native error
NativeError,
};