
After 20 years of writing JavaScript, I've seen many changes - from callback hell to async/await. But the upcoming JavaScript features will transform how we write code completely.
We've tested these proposals using transpilers and polyfills, ans the results are impressive: code that took 30 lines now takes 10, complex logic becomes readable at a glance, and even junior developers (and Devin 😅) can understand complex parts of our codebase.
If you've read my previous articles onHTML5 elements you didn't know you need orCSS modal windows, you know we like unusual tech atLingo.dev. These upcoming JavaScript features solve real problems that have annoyed developers for years.
Pipeline Operator Improves Code Readability
Complex data transformations in JavaScript often result in deeply nested function calls that are difficult to read and maintain. Developers must trace through nested functions from inside out, jumping between parentheses to understand the flow of data.
The pipeline operator (|>
) solves this problem by allowing data to flow through a series of operations in a clear, top-to-bottom manner:
// Instead of this nested messconstresult=saveToDatabase(validateUser(normalizeData(enrichUserProfile(user))));// You'll write thisconstresult=user|>enrichUserProfile|>normalizeData|>validateUser|>saveToDatabase;
Let's look at a more complex real-world example. Consider an image processing service that has grown over time:
// Before: Nested function hellfunctionprocessImage(image){returncompressImage(addWatermark(optimizeForWeb(applyFilter(resizeImage(image,{width:800,height:600}),'sepia')),'Copyright 2025'),0.8);}// After: Clean, readable pipelinefunctionprocessImage(image){returnimage|>(img)=>resizeImage(img,{width:800,height:600})|>(img)=>applyFilter(img,'sepia')|>optimizeForWeb|>(img)=>addWatermark(img,'Copyright 2025')|>(img)=>compressImage(img,0.8);}
Here's another practical example showing how the pipeline operator simplifies data processing for analytics:
// Before: Hard to follow the data flowfunctionanalyzeUserData(users){returngenerateReport(groupByMetric(filterInactiveUsers(normalizeUserData(users)),'registrationMonth'));}// After: Clear data transformation stepsfunctionanalyzeUserData(users){returnusers|>normalizeUserData|>filterInactiveUsers|>(data)=>groupByMetric(data,'registrationMonth')|>generateReport;}
The pipeline operator also makes it easier to insert debugging or logging between steps:
functionprocessPayment(payment){returnpayment|>validatePayment|>(result)=>{console.log(`Validation result:${JSON.stringify(result)}`);returnresult;}|>processTransaction|>(result)=>{console.log(`Transaction result:${JSON.stringify(result)}`);returnresult;}|>sendReceipt;}
How to Use It Today
The pipeline operator is currently at Stage 2 in the TC39 process as of May 2025. While not yet part of the official JavaScript specification, you can start using it today:
With Babel (Generic Setup):
# Install the pipeline operator pluginnpminstall--save-dev @babel/plugin-proposal-pipeline-operator
Add to your.babelrc
:
{"plugins":[["@babel/plugin-proposal-pipeline-operator",{"proposal":"hack","topicToken":"%"}]]}
With Vite:
# Install dependenciesnpminstall--save-dev vite-plugin-babel @babel/core @babel/plugin-proposal-pipeline-operator
// vite.config.jsimport{defineConfig}from'vite';importbabelfrom'vite-plugin-babel';exportdefaultdefineConfig({plugins:[babel({babelConfig:{plugins:[['@babel/plugin-proposal-pipeline-operator',{proposal:'hack',topicToken:'%'}]]}})]});
With Next.js:
# Install the pipeline operator pluginnpminstall--save-dev @babel/plugin-proposal-pipeline-operator
// .babelrc{"presets":["next/babel"],"plugins":[["@babel/plugin-proposal-pipeline-operator",{"proposal":"hack","topicToken":"%"}]]}
With tsup:
# Install dependenciesnpminstall--save-dev tsup @babel/core @babel/plugin-proposal-pipeline-operator
// tsup.config.tsimport{defineConfig}from'tsup';import*asbabelfrom'@babel/core';importfsfrom'fs';exportdefaultdefineConfig({entry:['src/index.ts'],format:['cjs','esm'],dts:true,esbuildPlugins:[{name:'babel',setup(build){build.onLoad({filter:/\.(jsx?|tsx?)$/},async(args)=>{constsource=awaitfs.promises.readFile(args.path,'utf8');constresult=awaitbabel.transformAsync(source,{filename:args.path,presets:[['@babel/preset-env',{targets:'defaults'}],'@babel/preset-typescript'],plugins:[['@babel/plugin-proposal-pipeline-operator',{proposal:'hack',topicToken:'%'}]]});return{contents:result?.code||'',loader:args.path.endsWith('x')?'jsx':'js'};});}}]});
With Remix (using Vite):
# Install dependenciesnpminstall--save-dev vite-plugin-babel @babel/core @babel/plugin-proposal-pipeline-operator
// vite.config.jsimport{defineConfig}from'vite';import{vitePluginasremix}from'@remix-run/dev';importbabelfrom'vite-plugin-babel';exportdefaultdefineConfig({plugins:[babel({babelConfig:{plugins:[['@babel/plugin-proposal-pipeline-operator',{proposal:'hack',topicToken:'%'}]]}}),remix()]});
Pattern Matching Simplifies Complex Conditionals
Complex if/else statements and switch cases quickly become unwieldy in large codebases. Functions with nested if/else blocks checking various object properties make it nearly impossible to verify all edge cases.
Pattern matching provides a direct solution to this problem.
This feature brings functional programming capabilities to JavaScript, allowing you to match and destructure complex data in a single operation:
functionprocessMessage(message){returnmatch(message){whenString(text)=>`Text message:${text}`,when[String(sender),String(content)]=>`Message from${sender}:${content}`,when{type:'error',content:String(text),code:Number(code)}=>`Error:${text} (Code:${code})`,when_=>'Unknown message format'};}
Without pattern matching, you'd need multiple if/else statements with type checking:
functionprocessMessage(message){// Check if it's a stringif(typeofmessage==='string'){return`Text message:${message}`;}// Check if it's an array with specific structureif(Array.isArray(message)&&message.length===2&&typeofmessage[0]==='string'&&typeofmessage[1]==='string'){return`Message from${message[0]}:${message[1]}`;}// Check if it's an object with specific propertiesif(message&&typeofmessage==='object'&&message.type==='error'&&typeofmessage.content==='string'&&typeofmessage.code==='number'){return`Error:${message.content} (Code:${message.code})`;}// Default casereturn'Unknown message format';}
Pattern matching excels when handling complex state transitions in modern web applications:
functionhandleUserAction(state,action){returnmatch([state,action]){when[{status:'idle'},{type:'FETCH_START'}]=>({status:'loading',data:state.data}),when[{status:'loading'},{type:'FETCH_SUCCESS',payload}]=>({status:'success',data:payload,error:null}),when[{status:'loading'},{type:'FETCH_ERROR',error}]=>({status:'error',data:null,error}),when[{status:'success'},{type:'REFRESH'}]=>({status:'loading',data:state.data}),when_=>state};}
The equivalent code without pattern matching is significantly more verbose:
functionhandleUserAction(state,action){// Check idle state + fetch startif(state.status==='idle'&&action.type==='FETCH_START'){return{status:'loading',data:state.data};}// Check loading state + fetch successif(state.status==='loading'&&action.type==='FETCH_SUCCESS'){return{status:'success',data:action.payload,error:null};}// Check loading state + fetch errorif(state.status==='loading'&&action.type==='FETCH_ERROR'){return{status:'error',data:null,error:action.error};}// Check success state + refreshif(state.status==='success'&&action.type==='REFRESH'){return{status:'loading',data:state.data};}// Default: return unchanged statereturnstate;}
Pattern matching also provides exhaustiveness checking - the compiler warns you if you've missed handling a possible case. This eliminates an entire class of bugs that plague traditional conditional logic.
Here's another practical example for parsing configuration formats:
functionparseConfig(config){returnmatch(config){when{version:1,settings:Object(settings)}=>parseV1Settings(settings),when{version:2,config:Object(settings)}=>parseV2Settings(settings),whenString(jsonString)=>parseConfig(JSON.parse(jsonString)),when[String(env),Object(overrides)]=>mergeConfigs(getEnvConfig(env),overrides),when_=>thrownewError(`Invalid configuration format:${JSON.stringify(config)}`)};}
How to Use It Today
Pattern matching is currently at Stage 1 in the TC39 process as of May 2025, which means it's still in the proposal phase with ongoing discussions about syntax and semantics. However, you can experiment with it today:
With Babel (Generic Setup):
# Install the pattern matching pluginnpminstall--save-dev babel-plugin-proposal-pattern-matching
Add to your.babelrc
:
{"plugins":["babel-plugin-proposal-pattern-matching"]}
With Vite:
# Install dependenciesnpminstall--save-dev vite-plugin-babel @babel/core babel-plugin-proposal-pattern-matching
// vite.config.jsimport{defineConfig}from'vite';importbabelfrom'vite-plugin-babel';exportdefaultdefineConfig({plugins:[babel({babelConfig:{plugins:['babel-plugin-proposal-pattern-matching']}})]});
With Next.js:
# Install the pattern matching pluginnpminstall--save-dev babel-plugin-proposal-pattern-matching
// .babelrc{"presets":["next/babel"],"plugins":["babel-plugin-proposal-pattern-matching"]}
With tsup:
# Install dependenciesnpminstall--save-dev tsup @babel/core babel-plugin-proposal-pattern-matching
// tsup.config.tsimport{defineConfig}from'tsup';import*asbabelfrom'@babel/core';importfsfrom'fs';exportdefaultdefineConfig({entry:['src/index.ts'],format:['cjs','esm'],dts:true,esbuildPlugins:[{name:'babel',setup(build){build.onLoad({filter:/\.(jsx?|tsx?)$/},async(args)=>{constsource=awaitfs.promises.readFile(args.path,'utf8');constresult=awaitbabel.transformAsync(source,{filename:args.path,presets:[['@babel/preset-env',{targets:'defaults'}],'@babel/preset-typescript'],plugins:['babel-plugin-proposal-pattern-matching']});return{contents:result?.code||'',loader:args.path.endsWith('x')?'jsx':'js'};});}}]});
Production Alternatives:
For production code today, use the ts-pattern library:
npminstallts-pattern
import{match,P}from'ts-pattern';functionprocessMessage(message:unknown){returnmatch(message).with(P.string,text=>`Text message:${text}`).with([P.string,P.string],([sender,content])=>`Message from${sender}:${content}`).with({type:'error',content:P.string,code:P.number},({content,code})=>`Error:${content} (Code:${code})`).otherwise(()=>'Unknown message format');}
Temporal API Solves Date Handling Problems
JavaScript's built-inDate
object has long been a source of frustration for developers. It's mutable (dates can be accidentally modified), has confusing month indexing (January is 0!), and timezone handling is problematic. These issues have led to the widespread use of libraries like Moment.js, date-fns, and Luxon.
The Temporal API provides a complete solution to these problems by reimagining date and time handling in JavaScript.
Here's a practical example of scheduling a meeting across timezones:
// Current approach with Date (likely to have bugs)functionscheduleMeeting(startDate,durationInMinutes,timeZone){conststart=newDate(startDate);constend=newDate(start.getTime()+durationInMinutes*60000);return{start:start.toISOString(),end:end.toISOString(),timeZone:timeZone// Not actually used in calculations};}// With Temporal APIfunctionscheduleMeeting(startDateTime,durationInMinutes,timeZone){conststart=Temporal.ZonedDateTime.from(startDateTime);constend=start.add({minutes:durationInMinutes});return{start:start.toString(),end:end.toString(),timeZone:start.timeZoneId// Properly tracked};}
For international flight booking systems, calculating flight durations across timezones becomes straightforward:
// Current approach with Date (error-prone)functioncalculateFlightDuration(departure,arrival,departureTimeZone,arrivalTimeZone){// Convert to milliseconds and calculate differenceconstdepartureTime=newDate(departure);constarrivalTime=newDate(arrival);// This doesn't account for timezone differences correctlyconstdurationMs=arrivalTime-departureTime;return{hours:Math.floor(durationMs/(1000*60*60)),minutes:Math.floor((durationMs%(1000*60*60))/(1000*60)),// No easy way to get arrival in departure's timezone};}// With Temporal APIfunctioncalculateFlightDuration(departure,arrival){constdepartureTime=Temporal.ZonedDateTime.from(departure);constarrivalTime=Temporal.ZonedDateTime.from(arrival);// Accurate duration calculation across time zonesconstduration=departureTime.until(arrivalTime);return{hours:duration.hours,minutes:duration.minutes,inLocalTime:arrivalTime.toLocaleString(),inDepartureTime:arrivalTime.withTimeZone(departureTime.timeZoneId).toLocaleString()};}
Here's another example showing how Temporal API handles recurring events, which are notoriously difficult with the current Date object:
// Current approach with Date (complex and error-prone)functiongetNextMeetingDates(startDate,count){constdates=[];constcurrent=newDate(startDate);for(leti=0;i<count;i++){dates.push(newDate(current));// Add 2 weeks - error-prone due to month boundaries, DST changes, etc.current.setDate(current.getDate()+14);}returndates;}// With Temporal APIfunctiongetNextMeetingDates(startDate,count){conststart=Temporal.PlainDate.from(startDate);constdates=[];for(leti=0;i<count;i++){constnextDate=start.add({days:i*14});dates.push(nextDate);}returndates;}
The Temporal API provides several key advantages:
- Immutability: All Temporal objects are immutable, preventing accidental modifications
- Separate types for different use cases: PlainDate, PlainTime, ZonedDateTime, etc.
- Intuitive methods: Clear, chainable methods for date arithmetic
- Proper timezone handling: Built-in support for timezones and daylight saving time
- Consistent behavior: Works the same way across all browsers
How to Use It Today
The Temporal API is at Stage 3 in the TC39 process as of May 2025, which means it's nearing completion and implementations are being developed. Here's how to use it today:
With the Official Polyfill:
npminstall @js-temporal/polyfill
// Import in your codeimport{Temporal}from'@js-temporal/polyfill';// Get the current date and timeconstnow=Temporal.Now.plainDateTimeISO();console.log(`Current time:${now.toString()}`);
With Vite:
# Install the Temporal API polyfillnpminstall @js-temporal/polyfill
// main.js or any entry fileimport{Temporal}from'@js-temporal/polyfill';// Now you can use Temporal API in your codeconstnow=Temporal.Now.plainDateTimeISO();console.log(`Current time:${now.toString()}`);
With Next.js:
# Install the Temporal API polyfillnpminstall @js-temporal/polyfill
// pages/_app.jsimport{Temporal}from'@js-temporal/polyfill';// Make Temporal available globally if neededif(typeofwindow!=='undefined'){window.Temporal=Temporal;}functionMyApp({Component,pageProps}){return<Component{...pageProps}/>;}exportdefaultMyApp;
With tsup:
# Install the Temporal API polyfillnpminstall @js-temporal/polyfill
// src/index.tsimport{Temporal}from'@js-temporal/polyfill';// Export it if you want to make it available to consumers of your packageexport{Temporal};// Your other code here
Browser Support:
- Chrome/Edge: Available behind the "Experimental JavaScript" flag
- Firefox: Available in Firefox 139 and later
- Safari: Not yet implemented
To check if Temporal is natively supported:
if(typeofTemporal!=='undefined'){console.log('Temporal API is natively supported');}else{console.log('Using polyfill');// Import polyfill}
Resource Management Eliminates Memory Leaks
JavaScript has long lacked deterministic resource cleanup. When working with files, database connections, or hardware access, developers must manually ensure resources are properly released - even in error cases.
This limitation leads to memory leaks and resource exhaustion bugs. The typical pattern involves try/finally blocks that quickly become unwieldy:
asyncfunctionprocessFile(path){letfile=null;try{file=awaitfs.promises.open(path,'r');constcontent=awaitfile.readFile({encoding:'utf8'});returnprocessContent(content);}finally{if(file){awaitfile.close();}}}
The newusing
andawait using
statements provide deterministic resource management in #"http://www.w3.org/2000/svg" width="20px" height="20px" viewbox="0 0 24 24">
Let's look at a more complex example with multiple resources that must be managed in a specific order:
// Current approach: Nested try/finally blocksasyncfunctionprocessData(dbConfig,filePath){letdb=null;letfile=null;try{db=awaitDatabase.connect(dbConfig);try{file=awaitfs.promises.open(filePath,'r');constdata=awaitfile.readFile({encoding:'utf8'});constprocessed=processRawData(data);returndb.store(processed);}finally{if(file){awaitfile.close();}}}finally{if(db){awaitdb.disconnect();}}}// With resource management: Clean and safeasyncfunctionprocessData(dbConfig,filePath){awaitusingdb=awaitDatabase.connect(dbConfig);awaitusingfile=awaitfs.promises.open(filePath,'r');constdata=awaitfile.readFile({encoding:'utf8'});constprocessed=processRawData(data);returndb.store(processed);// Resources are automatically cleaned up in reverse order:// 1. file is closed// 2. db is disconnected}
For database-heavy applications, this feature transforms connection pool management:
classDatabaseConnection{constructor(config){this.config=config;this.connection=null;}asyncconnect(){this.connection=awaitcreateConnection(this.config);returnthis;}asyncquery(sql,params){returnthis.connection.query(sql,params);}async[Symbol.asyncDispose](){if(this.connection){awaitthis.connection.close();this.connection=null;}}}// Using the connection with automatic cleanupasyncfunctiongetUserData(userId){awaitusingdb=awaitnewDatabaseConnection(config).connect();returndb.query('SELECT * FROM users WHERE id = ?',[userId]);// Connection is automatically closed when the function exits}
Here's another example showing how to manage hardware resources like WebUSB devices:
// Current approach: Manual cleanup requiredasyncfunctionreadFromUSBDevice(deviceFilter){letdevice=null;try{constdevices=awaitnavigator.usb.getDevices();device=devices.find(d=>d.productId===deviceFilter.productId);if(!device){device=awaitnavigator.usb.requestDevice({filters:[deviceFilter]});}awaitdevice.open();awaitdevice.selectConfiguration(1);awaitdevice.claimInterface(0);constresult=awaitdevice.transferIn(1,64);returnnewTextDecoder().decode(result.data);}finally{if(device){try{awaitdevice.close();}catch(e){console.error("Error closing device:",e);}}}}// With resource management: Automatic cleanupclassUSBDeviceResource{constructor(device){this.device=device;}staticasynccreate(deviceFilter){constdevices=awaitnavigator.usb.getDevices();letdevice=devices.find(d=>d.productId===deviceFilter.productId);if(!device){device=awaitnavigator.usb.requestDevice({filters:[deviceFilter]});}awaitdevice.open();awaitdevice.selectConfiguration(1);awaitdevice.claimInterface(0);returnnewUSBDeviceResource(device);}asyncread(){constresult=awaitthis.device.transferIn(1,64);returnnewTextDecoder().decode(result.data);}async[Symbol.asyncDispose](){try{awaitthis.device.close();}catch(e){console.error("Error closing device:",e);}}}asyncfunctionreadFromUSBDevice(deviceFilter){awaitusingdevice=awaitUSBDeviceResource.create(deviceFilter);returndevice.read();// Device is automatically closed when the function exits}
How to Use It Today
The Explicit Resource Management proposal (using/await using) is at Stage 3 in the TC39 process as of May 2025, which means it's nearing standardization. Here's how to use it today:
With Babel (Generic Setup):
# Install the resource management pluginnpminstall--save-dev @babel/plugin-proposal-explicit-resource-management
Add to your.babelrc
:
{"plugins":["@babel/plugin-proposal-explicit-resource-management"]}
With Vite:
# Install dependenciesnpminstall--save-dev vite-plugin-babel @babel/core @babel/plugin-proposal-explicit-resource-management
// vite.config.jsimport{defineConfig}from'vite';importbabelfrom'vite-plugin-babel';exportdefaultdefineConfig({plugins:[babel({babelConfig:{plugins:['@babel/plugin-proposal-explicit-resource-management']}})]});
With Next.js:
# Install the resource management pluginnpminstall--save-dev @babel/plugin-proposal-explicit-resource-management
// .babelrc{"presets":["next/babel"],"plugins":["@babel/plugin-proposal-explicit-resource-management"]}
With tsup:
# Install dependenciesnpminstall--save-dev tsup @babel/core @babel/plugin-proposal-explicit-resource-management
// tsup.config.tsimport{defineConfig}from'tsup';import*asbabelfrom'@babel/core';importfsfrom'fs';exportdefaultdefineConfig({entry:['src/index.ts'],format:['cjs','esm'],dts:true,esbuildPlugins:[{name:'babel',setup(build){build.onLoad({filter:/\.(jsx?|tsx?)$/},async(args)=>{constsource=awaitfs.promises.readFile(args.path,'utf8');constresult=awaitbabel.transformAsync(source,{filename:args.path,presets:[['@babel/preset-env',{targets:'defaults'}],'@babel/preset-typescript'],plugins:['@babel/plugin-proposal-explicit-resource-management']});return{contents:result?.code||'',loader:args.path.endsWith('x')?'jsx':'js'};});}}]});
Making Objects Disposable:
To make your objects work withusing
, implement theSymbol.dispose
method:
classMyResource{[Symbol.dispose](){// Cleanup code here}}
For async resources, implementSymbol.asyncDispose
:
classMyAsyncResource{async[Symbol.asyncDispose](){// Async cleanup code here}}
Polyfill for Symbols:
// polyfill.jsif(!Symbol.dispose){Symbol.dispose=Symbol("Symbol.dispose");}if(!Symbol.asyncDispose){Symbol.asyncDispose=Symbol("Symbol.asyncDispose");}
Decorators Add Functionality Without Changing Core Logic
JavaScript decorators provide a clean way to modify classes and methods with additional functionality. If you've used TypeScript or frameworks like Angular, you're already familiar with the concept. Now decorators are coming to vanilla JavaScript.
Decorators solve a fundamental problem: how to add cross-cutting concerns like logging, validation, or performance monitoring without cluttering your business logic.
Here's a practical example:
classUserController{@authenticate@rateLimit(100)@validate(userSchema)@logAccessasyncupdateUserProfile(userId,profileData){// The method is automatically wrapped with:// 1. Authentication check// 2. Rate limiting (100 requests per hour)// 3. Input validation against userSchema// 4. Access loggingconstuser=awaitthis.userRepository.findById(userId);Object.assign(user,profileData);returnthis.userRepository.save(user);}}
Without decorators, you'd need to manually wrap each method, resulting in code that's harder to read and maintain:
classUserController{asyncupdateUserProfile(userId,profileData){// Authentication checkif(!isAuthenticated()){thrownewError('Unauthorized');}// Rate limitingif(isRateLimited(this.constructor.name,'updateUserProfile',100)){thrownewError('Rate limit exceeded');}// Input validationconstvalidationResult=validateWithSchema(userSchema,profileData);if(!validationResult.valid){thrownewError(`Invalid data:${validationResult.errors.join(',')}`);}// LogginglogAccess(this.constructor.name,'updateUserProfile',userId);// Actual business logicconstuser=awaitthis.userRepository.findById(userId);Object.assign(user,profileData);returnthis.userRepository.save(user);}}
Here's a real-world example of using decorators for performance monitoring:
classDataProcessor{@measure@cacheprocessLargeDataset(data){// Complex and time-consuming operationreturndata.map(item=>/* complex transformation */).filter(item=>/* complex filtering */).reduce((acc,item)=>/* complex aggregation */);}}// Performance measurement decoratorfunctionmeasure(target,name,descriptor){constoriginal=descriptor.value;descriptor.value=function(...args){conststart=performance.now();constresult=original.apply(this,args);constend=performance.now();console.log(`${name} took${end-start}ms to execute`);returnresult;};returndescriptor;}// Caching decoratorfunctioncache(target,name,descriptor){constoriginal=descriptor.value;constcacheStore=newMap();descriptor.value=function(...args){constkey=JSON.stringify(args);if(cacheStore.has(key)){console.log(`Cache hit for${name}`);returncacheStore.get(key);}console.log(`Cache miss for${name}`);constresult=original.apply(this,args);cacheStore.set(key,result);returnresult;};returndescriptor;}
Decorators are particularly valuable for API development. Here's an example of using decorators to implement a RESTful API with proper error handling:
classProductAPI{@route('GET','/products')@paginate@handleErrorsasyncgetAllProducts(req){returnthis.productRepository.findAll();}@route('GET','/products/:id')@handleErrorsasyncgetProductById(req){constproduct=awaitthis.productRepository.findById(req.params.id);if(!product){thrownewNotFoundError(`Product with ID${req.params.id} not found`);}returnproduct;}@route('POST','/products')@validate(productSchema)@handleErrorsasynccreateProduct(req){returnthis.productRepository.create(req.body);}}// Route decoratorfunctionroute(method,path){returnfunction(target,name,descriptor){if(!target.constructor._routes){target.constructor._routes=[];}target.constructor._routes.push({method,path,handler:descriptor.value,name});returndescriptor;};}// Error handling decoratorfunctionhandleErrors(target,name,descriptor){constoriginal=descriptor.value;descriptor.value=asyncfunction(req,res){try{constresult=awaitoriginal.call(this,req);returnres.json(result);}catch(error){if(errorinstanceofNotFoundError){returnres.status(404).json({error:error.message});}if(errorinstanceofValidationError){returnres.status(400).json({error:error.message});}console.error(`Error in${name}:`,error);returnres.status(500).json({error:'Internal server error'});}};returndescriptor;}
How to Use Decorators Today
Decorators are at Stage 3 in the TC39 process as of May 2025, with implementations in progress in major browsers. Here's how to use them today:
With Babel (Generic Setup):
# Install the decorators pluginnpminstall--save-dev @babel/plugin-proposal-decorators
Add to your.babelrc
:
{"plugins":[["@babel/plugin-proposal-decorators",{"version":"2023-05"}]]}
With Vite:
# Install dependenciesnpminstall--save-dev vite-plugin-babel @babel/core @babel/plugin-proposal-decorators
// vite.config.jsimport{defineConfig}from'vite';importbabelfrom'vite-plugin-babel';exportdefaultdefineConfig({plugins:[babel({babelConfig:{plugins:[['@babel/plugin-proposal-decorators',{version:'2023-05'}]]}})]});
With Next.js:
# Install the decorators pluginnpminstall--save-dev @babel/plugin-proposal-decorators
// .babelrc{"presets":["next/babel"],"plugins":[["@babel/plugin-proposal-decorators",{"version":"2023-05"}]]}
With TypeScript:
Enable decorators intsconfig.json
:
{"compilerOptions":{"target":"ES2022","experimentalDecorators":true}}
With tsup:
# Install dependenciesnpminstall--save-dev tsup @babel/core @babel/plugin-proposal-decorators
// tsup.config.tsimport{defineConfig}from'tsup';import*asbabelfrom'@babel/core';importfsfrom'fs';exportdefaultdefineConfig({entry:['src/index.ts'],format:['cjs','esm'],dts:true,esbuildPlugins:[{name:'babel',setup(build){build.onLoad({filter:/\.(jsx?|tsx?)$/},async(args)=>{constsource=awaitfs.promises.readFile(args.path,'utf8');constresult=awaitbabel.transformAsync(source,{filename:args.path,presets:[['@babel/preset-env',{targets:'defaults'}],'@babel/preset-typescript'],plugins:[['@babel/plugin-proposal-decorators',{version:'2023-05'}]]});return{contents:result?.code||'',loader:args.path.endsWith('x')?'jsx':'js'};});}}]});
Important Note:
The decorator proposal has gone through several iterations. Make sure you're using the latest syntax (2023-05 version) as older versions are incompatible. The current specification defines three capabilities for decorators:
- They canreplace the decorated value with a matching value
- They can provideaccess to the decorated value
- They caninitialize the decorated value
Testing Your Configuration
After setting up your configuration, create a simple test file to verify that the features are working:
// test-features.js// Test pipeline operatorconstdouble=x=>x*2;constadd=x=>x+1;constsquare=x=>x*x;constresult=5|>double|>add|>square;console.log("Pipeline result:",result);// Should be 121// Test pattern matchingfunctionprocessValue(value){returnmatch(value){whenString(s)=>`String:${s}`,whenNumber(n)=>`Number:${n}`,when{type,data}=>`Object with type${type}`,when_=>'Unknown'};}console.log("Pattern matching:",processValue("test"),processValue(42),processValue({type:"user",data:{}}));// Test Temporal APIimport{Temporal}from'@js-temporal/polyfill';constnow=Temporal.Now.plainDateTimeISO();console.log("Current time:",now.toString());// Test resource managementclassResource{constructor(name){this.name=name;console.log(`Resource${name} created`);}[Symbol.dispose](){console.log(`Resource${this.name} disposed`);}}{usingresource=newResource("test");console.log("Using resource");}// Resource should be disposed here// Test decoratorsfunctionlog(target,name,descriptor){constoriginal=descriptor.value;descriptor.value=function(...args){console.log(`Calling${name} with${JSON.stringify(args)}`);returnoriginal.apply(this,args);};returndescriptor;}classCalculator{@logadd(a,b){returna+b;}}constcalc=newCalculator();console.log("Decorator result:",calc.add(2,3));
Conclusion
These upcoming JavaScript features aren't just syntactic sugar - they fundamentally change how we write code. AtLingo.dev, we've already seen how they can transform complex, error-prone code into clean, maintainable solutions.
The best part is you don't have to wait. With the right tools, you can start using these features today. Each one solves real problems that developers face daily, from managing resources to handling dates correctly.
What upcoming JavaScript feature are you most excited about? Have you found other ways to solve these problems in your codebase?
Share your thoughts!
Useful links:
- Lingo.dev on Twitter/X - dev + fun;
- GitHub repo - give us a star :)