You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Different result running native vs down-levelled async function #6068
In the following example, running original.js directly in MS Edge 25 / EdgeHTML 13 (with experimental JavaScript enabled) outputs "Get to the chopper!" to the console.
downlevel.js is produced by running typescript@next (I used v1.8.0-dev.20151210) over original.js with the --allowJs option. Running downlevel.js in MS Edge outputs an error SCRIPT445: Object doesn't support this action.
// file: tsconfig.json{"compilerOptions": {"target": "ES6","allowJs": true,"outFile": "downlevel.js"},"files": ["original.js"]}// file: original.jsvarPromise="I promise not to kill anyone";varfoo=async()=>"Get to the chopper!";foo().then(console.log);// file: downlevel.js (this one is output by tsc)var__awaiter=(this&&this.__awaiter)||function(thisArg,_arguments,Promise,generator){returnnewPromise(function(resolve,reject){generator=generator.call(thisArg,_arguments);functioncast(value){returnvalueinstanceofPromise&&value.constructor===Promise ? value : newPromise(function(resolve){resolve(value);});}functiononfulfill(value){try{step("next",value);}catch(e){reject(e);}}functiononreject(value){try{step("throw",value);}catch(e){reject(e);}}functionstep(verb,value){varresult=generator[verb](value);result.done ? resolve(result.value) : cast(result.value).then(onfulfill,onreject);}step("next",void0);});};varPromise="I promise not to kill anyone";varfoo=()=>__awaiter(this,void0,Promise,function*(){return"Get to the chopper!!";});foo().then(console.log);
It looks like the down-levelled code picks up the locally-scoped Promise instead of using the native Promise.
Brian Terlson (@bterlson) should Edge have errored on assigning to Promise, on executing the asynchronous function, or is it exhibiting correct behavior?
That issue talks about replacing/modifying global.Promise, which is legal but does not alter the fact that async functions still return an instance of the intrinsic %Promise%.
If changing global.Promise cannot change the return type of async functions, it seems logical that having a variable called Promise in local scope certainly won't change in either.
then when they actually use it, instead of just straight transpiling to Promise or Promise.resolve or .then, they can use these stored and unchangeable values.
(Also the TypeScript transpiler's "cast" function seems entirely unnecessary; just use Promise.resolve/PromiseResolve.)
Domenic Denicola (@domenic) Unfortunately getting the global object is not so easy as just saying "global", as some host environments expose the global object in different ways (e.g. window in the Browser, self in a WebWorker, etc.).
Troy Gerwien (@yortus) I don't think we will go to exhaustive lengths to resolve the native "Promise". We would have a similar issue for "Math" when using the transpiled ** operator.
Did you get any kind of error when you redefined Promise? We should be reporting an error if the Promise we resolve at design-time is not a PromiseConstructor.
Troy Gerwien (@yortus) This doesn't fail because we do not report semantic errors for JavaScript files. If you rename "original.js" to "original.ts" and run the sample, you get the following error:
error TS2403: Subsequent variable declarations must have the same type. Variable 'Promise' must be of type 'PromiseConstructor', but here has type 'string'.
I do see an issue that we do not report an error if "original.ts" is a module for an async function with an inferred return type. I will look into providing a better error for this.
Ron Buckton (@rbuckton) understandable re global/window checking being too hard. However if TSC just checks that Promise is of type PromiseConstructor, it's still trivially easy to produce valid ES6/ES7 code that does one thing when transpiled, and something different when run directly. For example
// file: test.tsimport*asPromisefrom'bluebird';varfoo=async()=>(awaitPromise.delay(500),42);foo().then(value=>console.log(`Result: ${value}`)).catch(error=>console.log(`Error: ${error}`)).finally(()=>console.log('All done!'));// OUTPUT of "node ./test.js" after running "tsc --target es6 --module commonjs"):// Result: 42// All done!// OUTPUT of running test.ts code directly in MS Edge (no tsc step):// SCRIPT438: Object doesn't support property or method 'finally'
It's not hard to see this happening in real code. People are starting to use ES7 async functions. And things like bluebird are still actively developed and have many useful additional features (like Promise.delay and Promise#finally used above). By habit or intention bluebird will still be imported into to module-level variable Promise. This is valid and won't have any impact on ES7 async function semantics.
Yet in this situation TSC produces code with different semantics, currently without any compiler warnings. The code may behave differently in unexpected ways depending how/whether it's transpiled. And we are talking about a situation with well-defined ES semantics (unless there are significant changes to https://tc39.github.io/ecmascript-asyncawait/ between stage 3 and 4).
The upshot of my previous comment is that the code in test.ts (above) has unambiguous ES6/ES7 semantics** and should fail, whether transpiled or not. And TSC should know at compile time that it will fail, because it statically knows async functions return ES6 Promises which don't have a finally method.
We have the same issue if you use --target ES5 --allowJs --module commonjs with this code:
// original.jsvarObject="What could possibly go wrong?";exportclassC{getX{return1;}}// downlevel.jsvarObject="What could possibly go wrong?";varC=(function(){functionC(){}Object.defineProperty(C.prototype,"X",{// throwsget: function(){return1;}enumerable: true,configurable: true});returnC;})();exports.C=C;
Troy Gerwien (@yortus) We cannot always guarantee 100% support for ES7 semantics when transpiling down to ES6/ES5. If you run your test.ts example above in an ES6 host, you wouldn't get an error. This is because the official spec proposal doesn't use the global Promise object either, but rather uses NewPromiseCapability. Once TypeScript supports --target ES7, we would use instead use native async functions.
What we can do is do the same thing we do for require and exports, and reserve Promise at the top level of a module that contains any async functions/methods. This means that if you want to use bluebird with async functions, you cannot import bluebird with the name Promise.
if you want to use bluebird with async functions, you cannot import bluebird with the name Promise.
I think that would be a good policy given how downlevelling is done. It will catch at compile time what could otherwise be confusing runtime errors.
we can [...] reserve Promise at the top level of a module that contains any async functions/methods
How about the following idea? Instead of reserving Promise at the top of files containing async functions, what about introducing a new helper like __getGlobalObject that gets the global object, then using it to reference _global.Promise when downlevelling async functions. Then the compiler would not need to raise an error for local Promise imports, which are after all valid code.
A quick google suggests getting the global object is not so hard as it seems, eg:
function__getGlobalObject(){try{// Get the global object in ES3/5/6/7, in either strict and non-strict modereturnFunction('return this')();}catch(e){// Must be running in browser with Content Security Policy headers setreturnwindow;}}
This helper might in fact be useful for transpiling any file that defines local aliases of global objects that are needed by other helpers, such as Object and Promise. All the sample code in this issue would retain runtime fidelity if transpiled this way.
As I understand it, using eval or Function wouldn't work in sandboxed environments (i.e. SES). If we needed to get the global object, what we've considered is:
var__global=typeofglobal!=="undefined" ? global : typeofself!=="undefined" ? self : undefined;
That still has issues with anyone locally defining "global" or "self" or "window", which puts us back in the same boat.
That's why I suggested adding a new helper like __getGlobalObject() { return Function('return this')(); } that can be overridden in projects that run in special environments.
That's why I suggested adding a new helper like __getGlobalObject() { return Function('return this')(); } that can be overridden in projects that run in special environments.
Out of curiosity, what would you replace it with in those environments? Is there any options when running under the strict prolog then?
Kitson Kelly (@kitsonk) probably return window would cover most cases, since CSP is a browser thing. But then web workers don't have window so would need return self.
I wrote in an earlier comment one possible default implementation that would cover most cases:
function__getGlobalObject(){try{// Get the global object in ES3/5/6/7, in either strict and non-strict modereturnFunction('return this')();}catch(e){// Must be running in browser with Content Security Policy headers setreturnwindow;}}
I spoke with Mohamed Hegazy (@mhegazy) and for now we will issue an error per this comment. I should have a PR for this shortly, once a prerequisite PR has been merged.
Obviously only some fraction of that will introduce async/await and hit this new error, but at least it shows it's fairly widespread practice to use a local Promise identifier (and valid ES6).
It will cause a lot of breakage in Dojo 2 I suspect, as we shim it without touching the global namespace, because we can't be sure we aren't running with other code that doesn't pollute it. Even if we don't use async/await, anyone wanting to use it will end up likely breaking their code and may not be totally clear on why.
Troy Gerwien (@yortus) The message is consistent with the error we report for using require or exports at the top level of a module as well. We are considering whether we should update the messaging around these errors across all similar cases as part of a different change at a later date.
Activity
DanielRosenwasser commentedon Dec 11, 2015
Brian Terlson (@bterlson) should Edge have errored on assigning to Promise, on executing the asynchronous function, or is it exhibiting correct behavior?
yortus commentedon Dec 12, 2015
Daniel Rosenwasser (@DanielRosenwasser) this seems to have been asked on tc39/ecmascript-asyncawait here, and answered in the comment thread for that issue by Domenic Denicola (@domenic) and Brian Terlson (@bterlson) here and here.
That issue talks about replacing/modifying
global.Promise, which is legal but does not alter the fact thatasyncfunctions still return an instance of the intrinsic %Promise%.If changing
global.Promisecannot change the return type ofasyncfunctions, it seems logical that having a variable calledPromisein local scope certainly won't change in either.domenic commentedon Dec 12, 2015
Ideally transpilers should do something like this in a "header" file that runs before any author code:
then when they actually use it, instead of just straight transpiling to
PromiseorPromise.resolveor.then, they can use these stored and unchangeable values.(Also the TypeScript transpiler's "cast" function seems entirely unnecessary; just use
Promise.resolve/PromiseResolve.)DanielRosenwasser commentedon Dec 23, 2015
When Ron Buckton (@rbuckton) gets back he should probably be in this discussion.
rbuckton commentedon Jan 22, 2016
Domenic Denicola (@domenic) Unfortunately getting the
globalobject is not so easy as just saying "global", as some host environments expose the global object in different ways (e.g.windowin the Browser,selfin a WebWorker, etc.).Troy Gerwien (@yortus) I don't think we will go to exhaustive lengths to resolve the native "Promise". We would have a similar issue for "Math" when using the transpiled
**operator.Did you get any kind of error when you redefined
Promise? We should be reporting an error if thePromisewe resolve at design-time is not aPromiseConstructor.rbuckton commentedon Jan 22, 2016
Troy Gerwien (@yortus) This doesn't fail because we do not report semantic errors for JavaScript files. If you rename "original.js" to "original.ts" and run the sample, you get the following error:
error TS2403: Subsequent variable declarations must have the same type. Variable 'Promise' must be of type 'PromiseConstructor', but here has type 'string'.I do see an issue that we do not report an error if "original.ts" is a module for an async function with an inferred return type. I will look into providing a better error for this.
yortus commentedon Jan 22, 2016
Ron Buckton (@rbuckton) understandable re
global/windowchecking being too hard. However if TSC just checks thatPromiseis of typePromiseConstructor, it's still trivially easy to produce valid ES6/ES7 code that does one thing when transpiled, and something different when run directly. For exampleIt's not hard to see this happening in real code. People are starting to use ES7 async functions. And things like
bluebirdare still actively developed and have many useful additional features (likePromise.delayandPromise#finallyused above). By habit or intentionbluebirdwill still be imported into to module-level variablePromise. This is valid and won't have any impact on ES7 async function semantics.Yet in this situation TSC produces code with different semantics, currently without any compiler warnings. The code may behave differently in unexpected ways depending how/whether it's transpiled. And we are talking about a situation with well-defined ES semantics (unless there are significant changes to https://tc39.github.io/ecmascript-asyncawait/ between stage 3 and 4).
yortus commentedon Jan 22, 2016
The upshot of my previous comment is that the code in
test.ts(above) has unambiguous ES6/ES7 semantics** and should fail, whether transpiled or not. And TSC should know at compile time that it will fail, because it statically knows async functions return ES6 Promises which don't have afinallymethod.** unless significant changes to https://tc39.github.io/ecmascript-asyncawait/ between stage 3 and 4
rbuckton commentedon Jan 22, 2016
We have the same issue if you use
--target ES5 --allowJs --module commonjswith this code:yortus commentedon Jan 22, 2016
Sure, but that's not the issue in
test.tsaboverbuckton commentedon Jan 23, 2016
Troy Gerwien (@yortus) We cannot always guarantee 100% support for ES7 semantics when transpiling down to ES6/ES5. If you run your
test.tsexample above in an ES6 host, you wouldn't get an error. This is because the official spec proposal doesn't use the global Promise object either, but rather uses NewPromiseCapability. Once TypeScript supports--target ES7, we would use instead use native async functions.What we can do is do the same thing we do for
requireandexports, and reservePromiseat the top level of a module that contains any async functions/methods. This means that if you want to use bluebird with async functions, you cannot import bluebird with the namePromise.Would report the error:
As a result, you would not be able to create a local binding for
Promise, and would instead need to do something like:yortus commentedon Jan 23, 2016
I think that would be a good policy given how downlevelling is done. It will catch at compile time what could otherwise be confusing runtime errors.
How about the following idea? Instead of reserving
Promiseat the top of files containing async functions, what about introducing a new helper like__getGlobalObjectthat gets the global object, then using it to reference_global.Promisewhen downlevelling async functions. Then the compiler would not need to raise an error for localPromiseimports, which are after all valid code.A quick google suggests getting the global object is not so hard as it seems, eg:
This helper might in fact be useful for transpiling any file that defines local aliases of global objects that are needed by other helpers, such as
ObjectandPromise. All the sample code in this issue would retain runtime fidelity if transpiled this way.rbuckton commentedon Jan 23, 2016
As I understand it, using
evalorFunctionwouldn't work in sandboxed environments (i.e. SES). If we needed to get the global object, what we've considered is:That still has issues with anyone locally defining "global" or "self" or "window", which puts us back in the same boat.
8 remaining items
DanielRosenwasser commentedon Jan 24, 2016
Kitson Kelly (@kitsonk) Yup, sorry about that. Thanks for the correction.
yortus commentedon Jan 25, 2016
Kitson Kelly (@kitsonk) Daniel Rosenwasser (@DanielRosenwasser)
Function('return this')()will throw an exception in browsers on a page with content securty policy that doesn't explicitly allowunsafe-eval(more info).That's why I suggested adding a new helper like
__getGlobalObject() { return Function('return this')(); }that can be overridden in projects that run in special environments.kitsonk commentedon Jan 25, 2016
Out of curiosity, what would you replace it with in those environments? Is there any options when running under the strict prolog then?
yortus commentedon Jan 25, 2016
Kitson Kelly (@kitsonk) probably
return windowwould cover most cases, since CSP is a browser thing. But then web workers don't havewindowso would needreturn self.I wrote in an earlier comment one possible default implementation that would cover most cases:
rbuckton commentedon Jan 26, 2016
I spoke with Mohamed Hegazy (@mhegazy) and for now we will issue an error per this comment. I should have a PR for this shortly, once a prerequisite PR has been merged.
yortus commentedon Jan 26, 2016
Reserving
Promiseshould probably be added to breaking changes if the PR lands in a release.Ron Buckton (@rbuckton) can you clarify if the PR you mentioned is being considered as a stopgap solution, or more final?
yortus commentedon Jan 27, 2016
Quick search on github for TypeScript code containing
import Promiseyields ~26000 results: https://github.com/search?l=typescript&q=import+Promise&ref=searchresults&type=Code&utf8=%E2%9C%93Obviously only some fraction of that will introduce async/await and hit this new error, but at least it shows it's fairly widespread practice to use a local
Promiseidentifier (and valid ES6).kitsonk commentedon Jan 27, 2016
It will cause a lot of breakage in Dojo 2 I suspect, as we shim it without touching the global namespace, because we can't be sure we aren't running with other code that doesn't pollute it. Even if we don't use async/await, anyone wanting to use it will end up likely breaking their code and may not be totally clear on why.
yortus commentedon Jan 27, 2016
Agreed. The error message they will see is:
Duplicate identifier 'Promise'. Compiler reserves name 'Promise' in the top level scope of a module containing async functions.When in fact
Promisehas no duplicate definition in the module's scope (unlike redefiningrequireorexportsfor example).Also it may be unclear for experienced JS devs why TS rejects this code, since it always worked before and is valid ES6.
rbuckton commentedon Jan 27, 2016
Troy Gerwien (@yortus) The message is consistent with the error we report for using
requireorexportsat the top level of a module as well. We are considering whether we should update the messaging around these errors across all similar cases as part of a different change at a later date.rbuckton commentedon Jan 27, 2016
This is fixed as of #6631. Please refer to the following comments as to the reasons and rationale for this change:
We will be updating our Breaking Changes documentation for TypeScript 1.8 to inform users as to the change and its implications.