The node-soap library is the go-to SOAP client for Node.js. It has been around since the early days of Node and handles most WSDL-based services well — but its error messages are often unhelpful, and some failures happen silently. This guide walks through every common error, explains why it happens, and gives you code that fixes it.
Error 1: TypeError: Cannot read properties of undefined (reading 'MyOperation')
This is the single most common node-soap error. You create a client, try to call an operation, and get a TypeError because the method doesn't exist on the client object.
const soap = require('soap');
soap.createClient('https://example.com/service?wsdl', (err, client) => {
// This crashes:
client.MyOperation({ id: 1 }, (err, result) => {
console.log(result);
});
});
TypeError: client.MyOperation is not a function
Why it happens:
- The operation name doesn't match: WSDL operation names are case-sensitive.
GetUseris not the same asgetUser. - The operation is under a specific service/port: Some WSDLs define multiple services or ports, and the operation is only available on a specific one.
- The WSDL didn't load correctly: The client was created but the WSDL was partially parsed.
Fix: Inspect available operations
soap.createClient('https://example.com/service?wsdl', (err, client) => {
if (err) {
console.error('Client creation failed:', err.message);
return;
}
// Print the full API structure
const description = client.describe();
console.log(JSON.stringify(description, null, 2));
// This shows:
// {
// "MyService": {
// "MyPort": {
// "GetUser": { "input": { ... }, "output": { ... } }
// }
// }
// }
// Call using the full path if needed
client.MyService.MyPort.GetUser({ id: 1 }, (err, result) => {
console.log(result);
});
});
Always call client.describe() first. It shows you the exact names of services, ports, and operations that node-soap parsed from the WSDL.
Error 2: Callback err is null but result is empty
This is a silent failure. The callback fires with no error, but the result object is empty or has unexpected structure.
client.GetUser({ UserId: 1 }, (err, result) => {
console.log('Error:', err); // null
console.log('Result:', result); // {} or { return: undefined }
});
Why it happens:
- Namespace mismatch: The request elements are in the wrong namespace, so the server ignores them and returns an empty response.
- Wrong parameter names: The WSDL expects
userIdbut you're sendingUserId. - SOAPAction header is wrong: The server uses SOAPAction to route requests, and node-soap is sending the wrong value.
Fix: Check the raw XML exchange
client.GetUser({ UserId: 1 }, (err, result, rawResponse, soapHeader, rawRequest) => {
// rawRequest shows what node-soap actually sent
console.log('Sent XML:\n', rawRequest);
// rawResponse shows what the server returned
console.log('Received XML:\n', rawResponse);
});
Fix: Override the SOAPAction
// Check what SOAPAction node-soap is using
client.GetUser({ UserId: 1 }, (err, result) => {
console.log('Last request headers:', client.lastRequestHeaders);
});
// Override it if needed
client.addHttpHeader('SOAPAction', 'http://example.com/IUserService/GetUser');
Error 3: WSDL Fetch Failure
Error: Invalid WSDL URL: https://example.com/service?wsdl
Code: ECONNREFUSED
Or sometimes:
Error: Parse Error
Common causes:
- Network issues: DNS failure, firewall blocking, VPN not connected.
- Authentication required: The WSDL URL requires HTTP Basic auth.
- The URL returns HTML: A reverse proxy or WAF redirects to a login page.
Fix: Pass authentication and custom HTTP options
const soap = require('soap');
const options = {
wsdl_headers: {
'Authorization': 'Basic ' + Buffer.from('user:pass').toString('base64'),
},
wsdl_options: {
timeout: 30000,
// For self-signed certificates
rejectUnauthorized: false, // Development only!
},
};
soap.createClient('https://example.com/service?wsdl', options, (err, client) => {
if (err) {
console.error('WSDL load failed:', err.message);
return;
}
console.log('Client created successfully');
});
Fix: Load WSDL from a local file
// Download first: curl -o service.wsdl "https://example.com/service?wsdl"
soap.createClient('./service.wsdl', (err, client) => {
// Set the actual endpoint URL since the local WSDL won't have it
client.setEndpoint('https://example.com/service');
// Now make calls as normal
});
Error 4: Async/Await Pattern Issues
node-soap was built around callbacks, and the Promise-based API has quirks that cause confusion.
Wrong: Using createClientAsync without error handling
// This can throw unhandled promise rejections
const client = await soap.createClientAsync('https://example.com/service?wsdl');
const result = await client.GetUserAsync({ id: 1 });
// result is an array: [output, rawResponse, soapHeader, rawRequest]
Correct: Proper async/await with destructuring
const soap = require('soap');
async function callSoapService() {
try {
const client = await soap.createClientAsync('https://example.com/service?wsdl');
// The Async suffix methods return an array, not just the result
const [result, rawResponse, soapHeader, rawRequest] = await client.GetUserAsync({ id: 1 });
console.log('Result:', result);
return result;
} catch (error) {
if (error.root && error.root.Envelope) {
// This is a SOAP Fault
const fault = error.root.Envelope.Body.Fault;
console.error('SOAP Fault:', fault.faultstring);
console.error('Fault detail:', JSON.stringify(fault.detail));
} else {
// This is a transport/network error
console.error('Transport error:', error.message);
}
throw error;
}
}
Key points about the async API:
createClientAsync()returns a Promise that resolves to the client.- Operation methods with the
Asyncsuffix (e.g.,GetUserAsync) return[result, rawResponse, soapHeader, rawRequest]. - SOAP Faults are thrown as errors with a
root.Envelopeproperty.
Error 5: SSL/TLS Configuration
Error: self-signed certificate in certificate chain
Or:
Error: unable to verify the first certificate
Fix: Configure SSL properly
const https = require('https');
const fs = require('fs');
const options = {
wsdl_options: {
// Custom CA certificate
ca: fs.readFileSync('/path/to/ca-cert.pem'),
// Or for mutual TLS
cert: fs.readFileSync('/path/to/client-cert.pem'),
key: fs.readFileSync('/path/to/client-key.pem'),
},
// These same options apply to runtime requests
request: require('request').defaults({
ca: fs.readFileSync('/path/to/ca-cert.pem'),
}),
};
soap.createClient('https://example.com/service?wsdl', options, (err, client) => {
// SSL is now configured for both WSDL fetching and SOAP calls
});
For the modern axios-based approach (node-soap >= 0.45):
const axios = require('axios');
const https = require('https');
const httpsAgent = new https.Agent({
ca: fs.readFileSync('/path/to/ca-cert.pem'),
rejectUnauthorized: true,
});
const options = {
request: axios.create({ httpsAgent }),
};
soap.createClient('https://example.com/service?wsdl', options, (err, client) => {
// ...
});
Error 6: Namespace and Prefix Issues
node-soap sometimes generates incorrect namespace prefixes, causing the server to reject the request or return empty results.
Fix: Override namespaces
soap.createClient('https://example.com/service?wsdl', (err, client) => {
// Add a namespace that node-soap missed
client.addSoapHeader({
'wsse:Security': {
attributes: {
'xmlns:wsse': 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd',
},
},
});
// Override the namespace for a specific element
client.GetUser({
attributes: {
'xmlns:tns': 'http://example.com/schemas',
},
'tns:UserId': 1,
}, (err, result) => {
console.log(result);
});
});
Fix: Use overrideRootElement for incorrect request wrappers
const options = {
overrideRootElement: {
namespace: 'tns',
xmlnsAttributes: [{
name: 'xmlns:tns',
value: 'http://example.com/schemas',
}],
},
};
client.GetUser({ UserId: 1 }, options, (err, result) => {
console.log(result);
});
Error Handling Best Practice
Here's a robust pattern that handles all common failure modes:
const soap = require('soap');
class SoapServiceClient {
constructor(wsdlUrl, options = {}) {
this.wsdlUrl = wsdlUrl;
this.options = options;
this.client = null;
}
async connect() {
this.client = await soap.createClientAsync(this.wsdlUrl, this.options);
return this;
}
async call(operation, params) {
if (!this.client) {
throw new Error('Client not connected. Call connect() first.');
}
const asyncMethod = this.client[`${operation}Async`];
if (!asyncMethod) {
const available = Object.keys(this.client.describe())
.flatMap(svc => Object.keys(this.client.describe()[svc])
.flatMap(port => Object.keys(this.client.describe()[svc][port])));
throw new Error(
`Operation "${operation}" not found. Available: ${available.join(', ')}`
);
}
try {
const [result] = await asyncMethod.call(this.client, params);
return result;
} catch (error) {
if (error.root?.Envelope?.Body?.Fault) {
const fault = error.root.Envelope.Body.Fault;
throw new Error(`SOAP Fault [${fault.faultcode}]: ${fault.faultstring}`);
}
throw error;
}
}
}
// Usage
const svc = new SoapServiceClient('https://example.com/service?wsdl');
await svc.connect();
const user = await svc.call('GetUser', { UserId: 1 });
How SOAPless Helps
Every error in this guide stems from the same root cause: your Node.js application has to understand WSDL, construct XML, manage namespaces, and handle SOAP-specific error formats. That's a lot of complexity for what should be a simple API call.
SOAPless eliminates this entire layer. You register your WSDL URL, and SOAPless generates REST JSON endpoints that you call with fetch() or any HTTP client. No node-soap dependency, no namespace debugging, no callback-vs-promise confusion. The JSON request and response formats are clean, and errors come back as standard HTTP status codes with JSON bodies — exactly what your Node.js application expects.
SOAPless also auto-generates an OpenAPI specification for your SOAP service, so you can use tools like Swagger UI or Postman to explore and test endpoints before writing code.