Adding the console.log in this recursive function prevents NodeJS from throwing a "Maximum call stack size exceeded" exception. Instead it just exits after a couple thousand iterations with no message (using Node v16.6.0 on Win10).
Why does the console.log change the exception throwing behavior, and how should I catch this exception without removing the console.log?
(In browsers it also throws an exception as I expected.)
function recursive(i) {
console.log(i)
return recursive(i + 1) * 2
}
try {
recursive(0)
} catch (ex) {
console.log(ex)
}
There is no expected exception here.
Semantically, this is an infinite loop, which is valid. If the JS engine doesn't do tail call optimization infinite recursion (like your example) will cause the call stack to overflow, but this is neither required nor desired. The ECMAScript standard even states how tail calls should be optimized, though this isn't implemented in most JS engines (except for JavaScriptCore used in Safari/WebKit). Whether Node emits the Maximum call stack size exceeded exception or not is nothing you should rely on.
In NestJS the standard approach of handling errors is to let errors propagate up to the exception filter(s) so that code is not littered with try-catch blocks and exception logging and error conversion can be performed in one place.
With the move to node16, an event is thrown when there is no catch on a promise. This change seemingly leads to putting catch statements on all async calls. Is there a better approach that leverage the same exception filters methodology?
I was following the guide here for setting up a presignup trigger.
However, when I used callback(null, event) my lambda function would never actually return and I would end up getting an error
{ code: 'UnexpectedLambdaException',
name: 'UnexpectedLambdaException',
message: 'arn:aws:lambda:us-east-2:642684845958:function:proj-dev-confirm-1OP5DB3KK5WTA failed with error Socket timeout while invoking Lambda function.' }
I found a similar link here that says to use context.done().
After switching it works perfectly fine.
What's the difference?
exports.confirm = (event, context, callback) => {
event.response.autoConfirmUser = true;
context.done(null, event);
//callback(null, event); does not work
}
Back in the original Lambda runtime environment for Node.js 0.10, Lambda provided helper functions in the context object: context.done(err, res) context.succeed(res) and context.fail(err).
This was formerly documented, but has been removed.
Using the Earlier Node.js Runtime v0.10.42 is an archived copy of a page that no longer exists in the Lambda documentation, that explains how these methods were used.
When the Node.js 4.3 runtime for Lambda was launched, these remained for backwards compatibility (and remain available but undocumented), and callback(err, res) was introduced.
Here's the nature of your problem, and why the two solutions you found actually seem to solve it.
Context.succeed, context.done, and context.fail however, are more than just bookkeeping – they cause the request to return after the current task completes and freeze the process immediately, even if other tasks remain in the Node.js event loop. Generally that’s not what you want if those tasks represent incomplete callbacks.
https://aws.amazon.com/blogs/compute/node-js-4-3-2-runtime-now-available-on-lambda/
So with callback, Lambda functions now behave in a more paradigmatically correct way, but this is a problem if you intend for certain objects to remain on the event loop during the freeze that occurs between invocations -- unlike the old (deprecated) done fail succeed methods, using the callback doesn't suspend things immediately. Instead, it waits for the event loop to be empty.
context.callbackWaitsForEmptyEventLoop -- default true -- was introduced so that you can set it to false for those cases where you want the Lambda function to return immediately after you call the callback, regardless of what's happening in the event loop. The default is true because false can mask bugs in your function and can cause very erratic/unexpected behavior if you fail to consider the implications of container reuse -- so you shouldn't set this to false unless and until you understand why it is needed.
A common reason false is needed would be a database connection made by your function. If you create a database connection object in a global variable, it will have an open socket, and potentially other things like timers, sitting on the event loop. This prevents the callback from causing Lambda to return a response, until these operations are also finished or the invocation timeout timer fires.
Identify why you need to set this to false, and if it's a valid reason, then it is correct to use it.
Otherwise, your code may have a bug that you need to understand and fix, such as leaving requests in flight or other work unfinished, when calling the callback.
So, how do we parse the Cognito error? At first, it seemed pretty unusual, but now it's clear that it is not.
When executing a function, Lambda will throw an error that the tasked timed out after the configured number of seconds. You should find this to be what happens when you test your function in the Lambda console.
Unfortunately, Cognito appears to have taken an internal design shortcut when invoking a Lambda function, and instead of waiting for Lambda to timeout the invocarion (which could tie up resources inside Cognito) or imposing its own explicit timer on the maximum duration Cognito will wait for a Lambda response, it's relying on a lower layer socket timer to constrain this wait... thus an "unexpected" error is thrown while invoking the timeout.
Further complicating interpreting the error message, there are missing quotes in the error, where the lower layer exception is interpolated.
To me, the problem would be much more clear if the error read like this:
'arn:aws:lambda:...' failed with error 'Socket timeout' while invoking Lambda function
This format would more clearly indicate that while Cognito was invoking the function, it threw an internal Socket timeout error (as opposed to Lambda encountering an unexpected internal error, which was my original -- and incorrect -- assumption).
It's quite reasonable for Cognito to impose some kind of response time limit on the Lambda function, but I don't see this documented. I suspect a short timeout on your Lambda function itself (making it fail more promptly) would cause Cognito to throw a somewhat more useful error, but in my mind, Cognito should have been designed to include logic to make this an expected, defined error, rather than categorizing it as "unexpected."
As an update the Runtime Node.js 10.x handler supports an async function that makes use of return and throw statements to return success or error responses, respectively. Additionally, if your function performs asynchronous tasks then you can return a Promise where you would then use resolve or reject to return a success or error, respectively. Either approach simplifies things by not requiring context or callback to signal completion to the invoker, so your lambda function could look something like this:
exports.handler = async (event) => {
// perform tasking...
const data = doStuffWith(event)
// later encounter an error situation
throw new Error('tell invoker you encountered an error')
// finished tasking with no errors
return { data }
}
Of course you can still use context but its not required to signal completion.
I'd like to be able to handle an async exception in thread A such that if another thread B calls throwTo, that call blocks until my handler in A has a chance to finish. As I understand it throwTo only blocks until the exception is "received".
I thought maybe some clever use of uninterruptibleMask might help me, but I'm stumped.
EDIT: I just noticed this:
The difference between using try and catch for recovery is that in
catch the handler is inside an implicit block (see "Asynchronous
Exceptions") which is important when catching asynchronous exceptions, ...
I thought that might be suggesting that catch would actually do what I'm looking for (I was using onException), but that doesn't seem to be the case. So as an additional question: what is meant by "the handler is inside an implicit block" here?
I am thinking about when exactly I need to reject a promise.
I found a couple of questions regarding this topic, but could not find a proper answer.
When should I reject a promise?
This article
http://howtonode.org/6666a4b74d7434144cff717c828be2c3953d46e7/promises
says:
Resolve: A successful Promise is 'resolved' which invokes the success listeners that are waiting and remembers the value that was resolved for future success listeners that are attached. Resolution correlates to a returned value.
Reject: When an error condition is encountered, a Promise is 'rejected' which invokes the error listeners that are waiting and remembers the value that was rejected for future error listeners that are attached. Rejection correlates to a thrown exception.
Is this the principle guideline?
That one only reject a promise if an exception occured?
But in case of a function like
findUserByEmail()
I'd would expect the function to return a user, so that I can continue the chain without verifying the result
findUserByEmail()
.then(sendWelcomeBackEmail)
.then(doSomeNiceStuff)
.then(etc..)
What are best / common practises?
In general you can think of rejecting as being analogous to a synchronous throw and fulfilling as being analogous to a synchronous return. You should reject whenever the function is unsuccessful in some way. That could be a timeout, a network error, incorrect input etc. etc.
Rejecting a promise, just like throwing an exception, is useful for control flow. It doesn't have to represent a truly unexpected error; it can represent a problem that you fully anticipate and handle:
function getProfile(email) {
return getProfileOverNetwork(email)
.then(null, function (err) {
//something went wrong getting the profile
if (err.code === 'NonExistantUser') {
return defaultUser;
} else if (profileCached(email)) {
return getProfileFromCache(email);//fall back to cached profile
} else {
throw err;//sometimes we don't have a nice way of handling it
}
})
}
The rejection lets us jump over the normal success behavior until we get to a method that knows how to handle it. As another example, we might have some function that's deeply nested at the bottom of the applications stack, which rejects. This might not be handled until the very top of the stack, where we could log it. The point is that rejections travel up the stack just like exceptions do in synchronous code.
In general, wherever possible, if you are struggling to write some asynchronous code, you should think "what would I write if this were synchronous". It's usually a fairly simple transformation to get from that, to the promised equivalent.
A nice example of where rejected promises might be used is in an exists method:
function exists(filePath) {
return stat(filePath) //where stat gets last updated time etc. of the file
.then(function () { return true; }, function () { return false; })
}
Notice how in this case, the rejection is totally expected and just means the file does not exist. Notice also how it parallels the synchronous function though:
function existsSync(filePath) {
try {
statSync(filePath);
return true;
} catch (ex) {
return false;
}
}
Your Example
Returning to your example:
I would normally chose to reject the promise resulting from findUserByEmail if no user was found. It's something you fully expect to happen sometimes, but it's the exception from the norm, and should probably be handled pretty similarly to all other errors. Similarly if I were writing a synchronous function I would have it throw an exception.
Sometimes it might be useful to just return null instead, but this would depend on your applications logic and is probably not the best way to go.
I know where you're coming from. Q and Q documentation can quite easily have you believe that deferred/promise rejection is all about exception handling.
This is not necessarily the case.
A deferred can be rejected for whatever reason your application requires.
Deferreds/promises are all about handling responses from asynchronous processes, and every asynchronous processes can result in a variety of outcomes - some of which are "successful" and some "unsuccessful". You may choose to reject your deferred - for whatever reason, regardless of whether the outcome was nominally successful or unsuccessful, and without an exception ever having been thrown, either in javascript or in the asynchronous process.
You may also choose to implement a timeout on an asynchronous process, in which case you might choose to reject a deferred without a response (successful or unsuccessful) having been received. In fact, for timeouts, this is what you would typically choose to do.